Generate scalar Typed Jerboa ABI wrappers
ober
b2ac2610228f171a104e9b38dccac3c17f88bb82
--- a/docs/jerboa-to-rust.md +++ b/docs/jerboa-to-rust.md @@ -29,7 +29,10 @@ Jerboa code can call typed compiled code safely. - Do not translate arbitrary dynamic Jerboa to Rust. - Do not expose Rust lifetimes directly in Typed Jerboa syntax. - Do not require users to edit generated Rust. -- Do not use `unsafe` except in small, generated runtime and FFI shims. +- Do not use `unsafe` except in small, generated runtime and FFI shims. Modern + Rust treats exported symbol names as an unsafe attribute; generated C ABI + wrappers therefore use `#[unsafe(no_mangle)]` and deny unsafe operations in + unsafe functions. - Do not make Rust crates the canonical source of typed modules. ## Pipeline --- a/docs/typed-jerboa.md +++ b/docs/typed-jerboa.md @@ -151,6 +151,10 @@ Current landing: records, variants, primitive function bodies, record accessors/constructors, variant constructors, variant predicates, and exhaustive variant `match`; native artifact builds and FFI wrappers are still future work. +- 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. - `support/typed-rust.ss`, `make typed-rust`, and `make typed-build` generate a disposable Cargo crate under `build/typed/rust`; `typed-build` runs `cargo build` against the generated crate. --- a/lib/jerboa/typed/rust.ss +++ b/lib/jerboa/typed/rust.ss @@ -304,6 +304,79 @@ ": " (rust-type (typed-param-type param)))) + (def (abi-safe-type? type) + (and (symbol? type) + (memq type '(Unit Bool Char Int Nat Fixnum Float)) + #t)) + + (def (abi-rust-type type) + (case type + [(Unit) "()"] + [(Bool) "bool"] + [(Char) "u32"] + [(Int) "i64"] + [(Nat) "u64"] + [(Fixnum) "isize"] + [(Float) "f64"] + [else (error 'typed-rust "unsupported ABI type" type)])) + + (def (abi-safe-def? def) + (and (abi-safe-type? (typed-def-return-type def)) + (let loop ([params (typed-def-params def)]) + (cond + [(null? params) #t] + [(abi-safe-type? (typed-param-type (car params))) + (loop (cdr params))] + [else #f])))) + + (def (exported-def? module def) + (and (memq (typed-def-name def) (typed-module-exports module)) + #t)) + + (def (module-abi-prefix module) + (join-strings + (map rust-symbol-name (typed-module-name module)) + "_")) + + (def (abi-wrapper-name module def) + (string-append + "jt_" + (module-abi-prefix module) + "_" + (rust-symbol-name (typed-def-name def)))) + + (def (emit-abi-param param) + (string-append + (rust-symbol-name (typed-param-name param)) + ": " + (abi-rust-type (typed-param-type param)))) + + (def (emit-abi-wrapper module def port) + (when (abi-safe-def? def) + (let* ([params (typed-def-params def)] + [param-names (map (lambda (param) + (rust-symbol-name (typed-param-name param))) + params)] + [call + (string-append + (rust-symbol-name (typed-def-name def)) + "(" + (join-strings param-names ", ") + ")")]) + (write-line port 0 "#[unsafe(no_mangle)]") + (write-line port 0 + (string-append + "pub extern \"C\" fn " + (abi-wrapper-name module def) + "(" + (join-strings (map emit-abi-param params) ", ") + ") -> " + (abi-rust-type (typed-def-return-type def)) + " {")) + (write-line port 1 call) + (write-line port 0 "}") + (newline port)))) + (def (emit-binary-chain op args) (cond [(null? args) (error 'typed-rust "operator needs operands" op)] @@ -559,7 +632,7 @@ (def (emit-rust-header port) (write-line port 0 "// Generated by Jerboa's typed Rust backend. Do not edit.") - (write-line port 0 "#![forbid(unsafe_code)]") + (write-line port 0 "#![deny(unsafe_op_in_unsafe_fn)]") (write-line port 0 "#![allow(unused_parens)]") (write-line port 0 "#![allow(unused_variables)]") (newline port)) @@ -568,6 +641,12 @@ (for-each (lambda (decl) (emit-declaration decl port)) + (typed-module-declarations module)) + (for-each + (lambda (decl) + (when (and (typed-def? decl) + (exported-def? module decl)) + (emit-abi-wrapper module decl port))) (typed-module-declarations module))) (def (emit-module module port) --- a/tests/test-typed-rust.ss +++ b/tests/test-typed-rust.ss @@ -23,13 +23,15 @@ (define calc-form '(typed-library (sample typed calc) (export zero add-one) - (def (zero) : Nat + (def (private-zero) : Nat 0) + (def (zero) : Nat + (private-zero)) (def (add-one (x : Nat)) : Nat (+ x 1)))) (define calc-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\npub fn zero() -> u64 {\n 0u64\n}\n\npub fn add_one(x: u64) -> u64 {\n (x + 1u64)\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 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") (define data-form '(typed-library (sample typed data) @@ -43,7 +45,7 @@ (Noop)))) (define data-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n pub id: u64,\n pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n Insert {\n at: u64,\n text: String,\n },\n Noop,\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\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n pub id: u64,\n pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n Insert {\n at: u64,\n text: String,\n },\n Noop,\n}\n\n") (define ops-form '(typed-library (sample typed ops) @@ -66,7 +68,7 @@ ((Noop) 0))))) (define ops-rust - "// Generated by Jerboa's typed Rust backend. Do not edit.\n#![forbid(unsafe_code)]\n#![allow(unused_parens)]\n#![allow(unused_variables)]\n\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n pub id: u64,\n pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n Insert {\n at: u64,\n text: String,\n },\n Noop,\n}\n\npub fn pane_id(pane: Pane) -> u64 {\n (pane).id\n}\n\npub fn make_insert(at: u64, text: String) -> EditOp {\n EditOp::Insert { at: at, text: text }\n}\n\npub fn make_noop() -> EditOp {\n EditOp::Noop\n}\n\npub fn edit_size(op: EditOp) -> u64 {\n match op { EditOp::Insert { at, text } => at, EditOp::Noop => 0u64, }\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\n#[derive(Clone, Debug, PartialEq)]\npub struct Pane {\n pub id: u64,\n pub focused_p: bool,\n}\n\n#[derive(Clone, Debug, PartialEq)]\npub enum EditOp {\n Insert {\n at: u64,\n text: String,\n },\n Noop,\n}\n\npub fn pane_id(pane: Pane) -> u64 {\n (pane).id\n}\n\npub fn make_insert(at: u64, text: String) -> EditOp {\n EditOp::Insert { at: at, text: text }\n}\n\npub fn make_noop() -> EditOp {\n EditOp::Noop\n}\n\npub fn edit_size(op: EditOp) -> u64 {\n match op { EditOp::Insert { at, text } => at, EditOp::Noop => 0u64, }\n}\n\n") (printf "--- Typed Jerboa Rust emitter tests ---~%")