From ac71cff3380bb1b4226c9779fbc662a0bdb2bcc4 Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 08:55:11 +0100 Subject: [PATCH 01/10] add failing test highlighting panic on invalid UTF-8 cstr Signed-off-by: Thierry Marianne --- tests-pl/ffi_utf8_panic.pl | 16 +++++++++++++ tests/scryer/issues.rs | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests-pl/ffi_utf8_panic.pl diff --git a/tests-pl/ffi_utf8_panic.pl b/tests-pl/ffi_utf8_panic.pl new file mode 100644 index 00000000..2ca32ce3 --- /dev/null +++ b/tests-pl/ffi_utf8_panic.pl @@ -0,0 +1,16 @@ +:- use_module(library(os)). +:- use_module(library(ffi)). + +init :- + read(Body), + term_variables(Body, [LIB]), + Body, + use_foreign_module(LIB, [ + 'ffi_invalid_utf8_cstr'([], cstr) + ]). + +test :- + ffi:'ffi_invalid_utf8_cstr'(Str), + write(Str), nl. + +:- initialization((init,test)). diff --git a/tests/scryer/issues.rs b/tests/scryer/issues.rs index cb321746..856da766 100644 --- a/tests/scryer/issues.rs +++ b/tests/scryer/issues.rs @@ -150,3 +150,50 @@ fn http_open_hanging() { "received response with status code:200\nreceived response with status code:200\nreceived response with status code:200\nreceived response with status code:200\nreceived response with status code:200\n" ); } + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_utf8_panic() { + let tmp_dir: &std::path::Path = env!("CARGO_TARGET_TMPDIR").as_ref(); + let name = "ffi_utf8_panic"; + let src = r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { + b"Invalid\xFFUTF8\x00".as_ptr() as *const _ + } + "##; + + let mut child = std::process::Command::new("rustc") + .stdin(std::process::Stdio::piped()) + .args(["--edition", "2024"]) + .arg(format!("--target={}", current_platform::CURRENT_PLATFORM)) + .arg("--crate-type=dylib") + .arg(format!("--crate-name={name}")) + .arg("--out-dir") + .arg(tmp_dir) + .arg("-") + .spawn() + .unwrap(); + + use std::io::Write; + child + .stdin + .take() + .unwrap() + .write_all(src.as_bytes()) + .unwrap(); + assert!(child.wait().unwrap().success()); + + let dynlib_path = tmp_dir.join(format!( + "{}{name}{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + )); + + crate::helper::load_module_test_with_input( + "tests-pl/ffi_utf8_panic.pl", + format!("LIB={dynlib_path:?}."), + // Evaluates to: 'Invalid\xFFUTF8\n' + "[73,110,118,97,108,105,100,255,85,84,70,56]\n", + ); +} From fae9ca1bb97e64cae54c92973a1fb0d3e116a5b3 Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 12:03:21 +0100 Subject: [PATCH 02/10] fix utf-8 panic Signed-off-by: Thierry Marianne --- src/machine/system_calls.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index edd08cb5..4b38c90b 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -5125,10 +5125,29 @@ impl Machine { unify!(self.machine_st, return_value, struct_value); } Value::CString(cstr) => { - let str_cell = resource_error_call_result!( - self.machine_st, - self.machine_st.heap.allocate_cstr(cstr.to_str().unwrap()) - ); + let bytes = cstr.to_bytes(); + + let str_cell = match std::str::from_utf8(bytes) { + Ok(valid_str) => resource_error_call_result!( + self.machine_st, + self.machine_st.heap.allocate_cstr(valid_str) + ), + Err(_) => { + let cells: Vec<_> = bytes + .iter() + .map(|&b| fixnum_as_cell!(Fixnum::build_with(b))) + .collect(); + + resource_error_call_result!( + self.machine_st, + sized_iter_to_heap_list( + &mut self.machine_st.heap, + cells.len(), + cells.into_iter() + ) + ) + } + }; unify!(self.machine_st, str_cell, return_value); } From 891f7fc8924e707c7b2ad46d90e0bba59d38a1e7 Mon Sep 17 00:00:00 2001 From: "Thierry M." Date: Wed, 4 Mar 2026 13:33:00 +0100 Subject: [PATCH 03/10] use C-string literal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bennet Bleßmann <3877590+Skgland@users.noreply.github.com> --- tests/scryer/issues.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scryer/issues.rs b/tests/scryer/issues.rs index 856da766..6bfd6871 100644 --- a/tests/scryer/issues.rs +++ b/tests/scryer/issues.rs @@ -159,7 +159,7 @@ fn ffi_utf8_panic() { let src = r##" #[unsafe(no_mangle)] extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { - b"Invalid\xFFUTF8\x00".as_ptr() as *const _ + c"Invalid\xFFUTF8".as_ptr() } "##; From fa21ff4813cde3fc60803c8faf40d9612c0a0d1a Mon Sep 17 00:00:00 2001 From: "Thierry M." Date: Wed, 4 Mar 2026 13:40:05 +0100 Subject: [PATCH 04/10] use CStr .to_str() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bennet Bleßmann <3877590+Skgland@users.noreply.github.com> --- src/machine/system_calls.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 4b38c90b..946380bc 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -5125,9 +5125,7 @@ impl Machine { unify!(self.machine_st, return_value, struct_value); } Value::CString(cstr) => { - let bytes = cstr.to_bytes(); - - let str_cell = match std::str::from_utf8(bytes) { + let str_cell = match cstr.to_str() { Ok(valid_str) => resource_error_call_result!( self.machine_st, self.machine_st.heap.allocate_cstr(valid_str) From 16ed1040d251cfc9d0fb38baad19b579548f2787 Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 13:57:17 +0100 Subject: [PATCH 05/10] move test to tests/scryer/ffi.rs Signed-off-by: Thierry Marianne --- tests/scryer/ffi.rs | 21 +++++++++++++++++++ tests/scryer/issues.rs | 46 ------------------------------------------ 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/tests/scryer/ffi.rs b/tests/scryer/ffi.rs index abe5c9b7..bbc24c50 100644 --- a/tests/scryer/ffi.rs +++ b/tests/scryer/ffi.rs @@ -302,3 +302,24 @@ fn ffi_heap() { r#"133742"#, ); } + +#[test] +#[cfg_attr(miri, ignore = "ffi")] +fn ffi_utf8_panic() { + let dynlib_path = build_dynamic_library( + "ffi_utf8_panic", + r##" + #[unsafe(no_mangle)] + extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { + b"Invalid\xFFUTF8\x00".as_ptr() as *const _ + } + "##, + ); + + load_module_test_with_input( + "tests-pl/ffi_utf8_panic.pl", + format!("LIB={dynlib_path:?}."), + // Evaluates to: 'Invalid\xFFUTF8\n' + "[73,110,118,97,108,105,100,255,85,84,70,56]\n", + ); +} diff --git a/tests/scryer/issues.rs b/tests/scryer/issues.rs index 6bfd6871..bedaa9bf 100644 --- a/tests/scryer/issues.rs +++ b/tests/scryer/issues.rs @@ -151,49 +151,3 @@ fn http_open_hanging() { ); } -#[test] -#[cfg_attr(miri, ignore = "ffi")] -fn ffi_utf8_panic() { - let tmp_dir: &std::path::Path = env!("CARGO_TARGET_TMPDIR").as_ref(); - let name = "ffi_utf8_panic"; - let src = r##" - #[unsafe(no_mangle)] - extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { - c"Invalid\xFFUTF8".as_ptr() - } - "##; - - let mut child = std::process::Command::new("rustc") - .stdin(std::process::Stdio::piped()) - .args(["--edition", "2024"]) - .arg(format!("--target={}", current_platform::CURRENT_PLATFORM)) - .arg("--crate-type=dylib") - .arg(format!("--crate-name={name}")) - .arg("--out-dir") - .arg(tmp_dir) - .arg("-") - .spawn() - .unwrap(); - - use std::io::Write; - child - .stdin - .take() - .unwrap() - .write_all(src.as_bytes()) - .unwrap(); - assert!(child.wait().unwrap().success()); - - let dynlib_path = tmp_dir.join(format!( - "{}{name}{}", - std::env::consts::DLL_PREFIX, - std::env::consts::DLL_SUFFIX - )); - - crate::helper::load_module_test_with_input( - "tests-pl/ffi_utf8_panic.pl", - format!("LIB={dynlib_path:?}."), - // Evaluates to: 'Invalid\xFFUTF8\n' - "[73,110,118,97,108,105,100,255,85,84,70,56]\n", - ); -} From ed59aa05b0cf94c785fbdfa998c0200ab00eb79e Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 13:58:08 +0100 Subject: [PATCH 06/10] apply suggestion Signed-off-by: Thierry Marianne --- src/machine/system_calls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 946380bc..9b714d01 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -5131,7 +5131,7 @@ impl Machine { self.machine_st.heap.allocate_cstr(valid_str) ), Err(_) => { - let cells: Vec<_> = bytes + let cells: Vec<_> = cstr.to_bytes() .iter() .map(|&b| fixnum_as_cell!(Fixnum::build_with(b))) .collect(); From 4bd0ff0fcb097df786271e6883125698a3ff6e1e Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 14:04:47 +0100 Subject: [PATCH 07/10] cargo fmt fixes Signed-off-by: Thierry Marianne --- src/machine/system_calls.rs | 3 ++- tests/scryer/issues.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 9b714d01..1766e579 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -5131,7 +5131,8 @@ impl Machine { self.machine_st.heap.allocate_cstr(valid_str) ), Err(_) => { - let cells: Vec<_> = cstr.to_bytes() + let cells: Vec<_> = cstr + .to_bytes() .iter() .map(|&b| fixnum_as_cell!(Fixnum::build_with(b))) .collect(); diff --git a/tests/scryer/issues.rs b/tests/scryer/issues.rs index bedaa9bf..cb321746 100644 --- a/tests/scryer/issues.rs +++ b/tests/scryer/issues.rs @@ -150,4 +150,3 @@ fn http_open_hanging() { "received response with status code:200\nreceived response with status code:200\nreceived response with status code:200\nreceived response with status code:200\nreceived response with status code:200\n" ); } - From e00112b92f4a7e5fbc190760854ac2eed6622abc Mon Sep 17 00:00:00 2001 From: "Thierry M." Date: Wed, 4 Mar 2026 15:01:23 +0100 Subject: [PATCH 08/10] use C-string literal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bennet Bleßmann <3877590+Skgland@users.noreply.github.com> --- tests/scryer/ffi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scryer/ffi.rs b/tests/scryer/ffi.rs index bbc24c50..f91be5e2 100644 --- a/tests/scryer/ffi.rs +++ b/tests/scryer/ffi.rs @@ -311,7 +311,7 @@ fn ffi_utf8_panic() { r##" #[unsafe(no_mangle)] extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { - b"Invalid\xFFUTF8\x00".as_ptr() as *const _ + c"Invalid\xFFUTF8".as_ptr() } "##, ); From 817c584993a8fb2f39e248a2714159593c183047 Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Wed, 4 Mar 2026 15:06:27 +0100 Subject: [PATCH 09/10] revise indentation of raw string Signed-off-by: Thierry Marianne --- tests/scryer/ffi.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/scryer/ffi.rs b/tests/scryer/ffi.rs index f91be5e2..979df80c 100644 --- a/tests/scryer/ffi.rs +++ b/tests/scryer/ffi.rs @@ -309,11 +309,11 @@ fn ffi_utf8_panic() { let dynlib_path = build_dynamic_library( "ffi_utf8_panic", r##" - #[unsafe(no_mangle)] - extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { - c"Invalid\xFFUTF8".as_ptr() - } - "##, +#[unsafe(no_mangle)] +extern "C" fn ffi_invalid_utf8_cstr() -> *const core::ffi::c_char { + c"Invalid\xFFUTF8".as_ptr() +} + "##, ); load_module_test_with_input( From ead2fccdbb18190e466bd4b23844d72b6966f179 Mon Sep 17 00:00:00 2001 From: Thierry Marianne Date: Thu, 5 Mar 2026 09:02:59 +0100 Subject: [PATCH 10/10] add documentation Signed-off-by: Thierry Marianne --- src/lib/ffi.pl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/ffi.pl b/src/lib/ffi.pl index 78545094..e3128010 100644 --- a/src/lib/ffi.pl +++ b/src/lib/ffi.pl @@ -68,6 +68,11 @@ ffi:FUNCTION_NAME(+InputArg1, ..., +InputArgN). % for void and bool - When using `cstr` as an argument type the string will be deallocated once the function returns. - When using `cstr` as a return type the string will be copied and won't be deallocated. +- When an ffi function returns bytes that are not a valid utf8-string the bytes will be turned into a list of `codes` (integers) + instead of a string (list of `chars`). Note: passing a list of `codes` is not accepted in argument position. +- In argument position you can also pass a pointer directly instead of a string, + e.g. to pass a null-pointer one can provide the integer 0 as the argument. +- In return position a null-pointer will be returned as the integer 0 ## Example