Catch Typed Jerboa Rust ABI panics
ober
60aa985e50449aa7a42f0fb1dd17f294da9f167c
--- a/docs/jerboa-to-rust.md +++ b/docs/jerboa-to-rust.md @@ -235,7 +235,9 @@ Current landing: scalar ABI-safe exported functions get generated Rust Chez `foreign-procedure`. `String` and `Bytes` arguments can cross this boundary for scalar-return functions as bytevector-plus-length pairs, with `String` using UTF-8 conversion. Non-scalar returns and owned values still wait -for opaque handles and conversion records. +for opaque handles and conversion records. ABI wrappers catch Rust panics before +they cross Chez FFI and currently return conservative default scalar values; +structured error returns are still future work. ## Generics @@ -394,6 +396,10 @@ pub extern "C" fn jt_call(...) -> JtResultHandle { } ``` +Current landing catches panics in scalar wrappers but does not yet return +`JtResultHandle`; panic paths return conservative defaults such as `0`, `#f` +equivalent booleans, or replacement-character code points. + ## Unsafe Policy Generated Rust should be safe by default. --- a/docs/typed-jerboa.md +++ b/docs/typed-jerboa.md @@ -154,7 +154,9 @@ Current landing: - Scalar-only exported functions get initial `extern "C"` Rust wrappers. Rust requires the symbol export marker to be written as `#[unsafe(no_mangle)]`, so generated crates deny unsafe operations rather than claiming a blanket - `forbid(unsafe_code)` once wrappers are present. + `forbid(unsafe_code)` once wrappers are present. ABI wrappers catch Rust + panics before they cross Chez FFI and return conservative default values until + the structured error ABI lands. - `(jerboa typed wrapper)`, `support/typed-wrappers.ss`, and `make typed-wrappers` generate `.ss` Jerboa wrapper files for ABI-safe exports. The wrappers load the compiled Rust cdylib from `JERBOA_TYPED_RUST_LIB`, --- a/lib/jerboa/typed/rust.ss +++ b/lib/jerboa/typed/rust.ss @@ -370,7 +370,7 @@ (let ([name (rust-symbol-name (typed-param-name param))]) (case (typed-param-type param) [(Char) - (write-line port 1 + (write-line port 2 (string-append "let " name @@ -378,29 +378,29 @@ name ").unwrap_or('\\u{FFFD}');"))] [(String Bytes) - (write-line port 1 (string-append "let " name " = {")) - (write-line port 2 + (write-line port 2 (string-append "let " name " = {")) + (write-line port 3 (string-append "let bytes: &[u8] = if " name "_ptr.is_null() {")) - (write-line port 3 "&[]") - (write-line port 2 "} else {") - (write-line port 3 + (write-line port 4 "&[]") + (write-line port 3 "} else {") + (write-line port 4 "// unsafe: pointer and length are produced by the generated Jerboa wrapper.") - (write-line port 3 + (write-line port 4 (string-append "unsafe { std::slice::from_raw_parts(" name "_ptr, " name "_len) }")) - (write-line port 2 "};") - (write-line port 2 + (write-line port 3 "};") + (write-line port 3 (if (eq? (typed-param-type param) 'String) "String::from_utf8_lossy(bytes).into_owned()" "bytes.to_vec()")) - (write-line port 1 "};")] + (write-line port 2 "};")] [else #f]))) (def (abi-return-expression def call) @@ -408,6 +408,17 @@ [(Char) (string-append "(" call " as u32)")] [else call])) + (def (abi-default-expression type) + (case type + [(Unit) "()"] + [(Bool) "false"] + [(Char) "0xFFFDu32"] + [(Int) "0i64"] + [(Nat) "0u64"] + [(Fixnum) "0isize"] + [(Float) "0.0f64"] + [else (error 'typed-rust "unsupported ABI default type" type)])) + (def (emit-abi-wrapper module def port) (when (abi-safe-def? def) (let* ([params (typed-def-params def)] @@ -430,11 +441,21 @@ ") -> " (abi-rust-type (typed-def-return-type def)) " {")) + (write-line port 1 + "match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {") (for-each (lambda (param) (emit-abi-param-conversion param port)) params) - (write-line port 1 (abi-return-expression def call)) + (write-line port 2 (abi-return-expression def call)) + (write-line port 1 "})) {") + (write-line port 2 "Ok(value) => value,") + (write-line port 2 + (string-append + "Err(_) => " + (abi-default-expression (typed-def-return-type def)) + ",")) + (write-line port 1 "}") (write-line port 0 "}") (newline port)))) --- a/tests/test-typed-rust.ss +++ b/tests/test-typed-rust.ss @@ -31,7 +31,7 @@ (+ x 1)))) (define calc-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn private_zero() -> u64 {\n 0u64\n}\n\npub fn zero() -> u64 {\n private_zero()\n}\n\npub fn add_one(x: u64) -> u64 {\n (x + 1u64)\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_zero() -> u64 {\n zero()\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_add_one(x: u64) -> u64 {\n add_one(x)\n}\n\n") + "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn private_zero() -> u64 {\n 0u64\n}\n\npub fn zero() -> u64 {\n private_zero()\n}\n\npub fn add_one(x: u64) -> u64 {\n (x + 1u64)\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_zero() -> u64 {\n match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n zero()\n })) {\n Ok(value) => value,\n Err(_) => 0u64,\n }\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_calc_add_one(x: u64) -> u64 {\n match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n add_one(x)\n })) {\n Ok(value) => value,\n Err(_) => 0u64,\n }\n}\n\n") (define data-form '(typed-library (sample typed data) @@ -54,7 +54,7 @@ ch))) (define char-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn same_char(ch: char) -> char {\n ch\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_char_same_char(ch: u32) -> u32 {\n let ch = char::from_u32(ch).unwrap_or('\\u{FFFD}');\n (same_char(ch) as u32)\n}\n\n") + "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn same_char(ch: char) -> char {\n ch\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_char_same_char(ch: u32) -> u32 {\n match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n let ch = char::from_u32(ch).unwrap_or('\\u{FFFD}');\n (same_char(ch) as u32)\n })) {\n Ok(value) => value,\n Err(_) => 0xFFFDu32,\n }\n}\n\n") (define string-form '(typed-library (sample typed text) @@ -63,7 +63,7 @@ (string-length s)))) (define string-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn text_length(s: String) -> u64 {\n (s).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_text_text_length(s_ptr: *const u8, s_len: usize) -> u64 {\n let s = {\n let bytes: &[u8] = if s_ptr.is_null() {\n &[]\n } else {\n // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n unsafe { std::slice::from_raw_parts(s_ptr, s_len) }\n };\n String::from_utf8_lossy(bytes).into_owned()\n };\n text_length(s)\n}\n\n") + "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn text_length(s: String) -> u64 {\n (s).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_text_text_length(s_ptr: *const u8, s_len: usize) -> u64 {\n match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n let s = {\n let bytes: &[u8] = if s_ptr.is_null() {\n &[]\n } else {\n // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n unsafe { std::slice::from_raw_parts(s_ptr, s_len) }\n };\n String::from_utf8_lossy(bytes).into_owned()\n };\n text_length(s)\n })) {\n Ok(value) => value,\n Err(_) => 0u64,\n }\n}\n\n") (define bytes-form '(typed-library (sample typed bytes) @@ -72,7 +72,7 @@ (bytevector-length data)))) (define bytes-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn bytes_length(data: Vec<u8>) -> u64 {\n (data).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_bytes_bytes_length(data_ptr: *const u8, data_len: usize) -> u64 {\n let data = {\n let bytes: &[u8] = if data_ptr.is_null() {\n &[]\n } else {\n // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n unsafe { std::slice::from_raw_parts(data_ptr, data_len) }\n };\n bytes.to_vec()\n };\n bytes_length(data)\n}\n\n") + "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![deny(unsafe_op_in_unsafe_fn)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn bytes_length(data: Vec<u8>) -> u64 {\n (data).len() as u64\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn jt_sample_typed_bytes_bytes_length(data_ptr: *const u8, data_len: usize) -> u64 {\n match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n let data = {\n let bytes: &[u8] = if data_ptr.is_null() {\n &[]\n } else {\n // unsafe: pointer and length are produced by the generated Jerboa wrapper.\n unsafe { std::slice::from_raw_parts(data_ptr, data_len) }\n };\n bytes.to_vec()\n };\n bytes_length(data)\n })) {\n Ok(value) => value,\n Err(_) => 0u64,\n }\n}\n\n") (define ops-form '(typed-library (sample typed ops)