Support Typed Jerboa string returns

ober

2d1d97d785dd1dc150bc52c174fce26c43df8743

diff --git a/docs/jerboa-to-rust.md b/docs/jerboa-to-rust.md
index 499b19a..0f32d9f 100644
--- a/docs/jerboa-to-rust.md
+++ b/docs/jerboa-to-rust.md
@@ -233,13 +233,16 @@ should get separate ABI wrappers.
 Current landing: scalar ABI-safe exported functions get generated Rust
 `extern "C"` wrappers, and `.ss` Jerboa wrapper files call those symbols through
 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. Same-module record and variant values can cross
-exported typed function boundaries as opaque `u64` handles stored in a generated
-Rust registry and tagged by the generated Jerboa wrappers. Other non-scalar
-returns, cross-module owned values, and conversion records remain future work.
-ABI wrappers catch Rust panics before they cross Chez FFI and currently return
-conservative default values; structured error returns are still future work.
+boundary as bytevector-plus-length pairs, with `String` using UTF-8 conversion.
+`String` and `Bytes` return values cross back through out pointer/length
+parameters, a generated Rust-owned byte buffer, and a generated destructor that
+the Scheme wrapper calls after copying into Chez-owned memory. Same-module
+record and variant values can cross exported typed function boundaries as
+opaque `u64` handles stored in a generated Rust registry and tagged by the
+generated Jerboa wrappers. Other non-scalar returns, cross-module owned values,
+and conversion records remain future work. ABI wrappers catch Rust panics before
+they cross Chez FFI and currently return conservative default values;
+structured error returns are still future work.
 
 ## Generics
 
@@ -350,8 +353,9 @@ pub struct JtString {
 
 For strings, define clear ownership:
 
-- Jerboa-to-Rust string arguments are copied in the MVP.
-- Rust-to-Jerboa strings are returned with a destructor function.
+- Jerboa-to-Rust string arguments are copied from generated UTF-8 bytevectors.
+- Rust-to-Jerboa strings are returned as Rust-owned byte buffers, copied by the
+  generated Scheme wrapper, and then released through `jt_byte_buffer_free`.
 - Later, add borrowed string views with lifetime restrictions.
 
 ## Generated Jerboa Wrappers
@@ -584,8 +588,9 @@ Second module: typed `rope`.
   `(Bytes -> Nat)` builtins.
 - Generate wrappers. Initial scalar `.ss` wrappers plus `String` and `Bytes`
   argument wrappers landed. Same-module record and variant values now cross the
-  wrapper boundary as opaque handles. String/bytes return, option, result, and
-  richer handle conversions remain future work.
+  wrapper boundary as opaque handles. `String` and `Bytes` returns now cross
+  through generated byte buffer ownership helpers. Option, result, and richer
+  handle conversions remain future work.
 
 ### Milestone 3: Records and Variants
 
diff --git a/docs/typed-jerboa.md b/docs/typed-jerboa.md
index 1bba4aa..9a29797 100644
--- a/docs/typed-jerboa.md
+++ b/docs/typed-jerboa.md
@@ -163,15 +163,18 @@ Current landing:
   validate dynamic arguments, call `foreign-procedure`, convert `Char` values
   through unsigned code points, and pass `String` arguments as UTF-8
   bytevector-plus-length pairs and `Bytes` arguments as bytevector-plus-length
-  pairs for scalar-return functions.
+  pairs. `String` and `Bytes` return values now cross back through a generated
+  Rust-owned byte buffer plus destructor; the Scheme wrapper copies the bytes
+  into Chez-owned memory and frees the Rust buffer.
 - Exported typed `def` functions can now cross records and variants through an
   opaque `u64` handle registry when the record or variant type is declared in
   the same typed module. Generated Scheme wrappers tag handles by typed name and
   reject wrong-handle calls before FFI.
 - `make typed-wrapper-smoke` builds the primitive Rust fixture, generates its
   wrapper, loads the cdylib, and calls the generated Jerboa functions through
-  Chez FFI. It also checks that bad dynamic calls are rejected by generated
-  wrapper predicates before crossing the FFI boundary.
+  Chez FFI, including returned strings, returned bytevectors, and opaque
+  record/variant handles. It also checks that bad dynamic calls are rejected by
+  generated wrapper predicates before crossing the FFI boundary.
 - `support/typed-rust.ss`, `make typed-rust`, and `make typed-build` generate a
   disposable Cargo crate under `build/typed/rust`; `typed-build` also writes
   wrappers under `build/typed/jerboa` and runs `cargo build` against the
@@ -821,12 +824,13 @@ Minimum excluded features:
 - Compile generated Rust as a static or dynamic library. Initial disposable
   `rlib`/`staticlib`/`cdylib` builds landed.
 - Generate conversion functions. Initial scalar argument checks, `Char`
-  code-point conversions, `String` argument UTF-8 bytevector conversions, and
-  `Bytes` argument bytevector conversions landed at the wrapper boundary.
+  code-point conversions, `String` argument/return UTF-8 bytevector
+  conversions, and `Bytes` argument/return bytevector conversions landed at the
+  wrapper boundary.
 - Generate Jerboa wrappers. Initial `.ss` wrapper generation landed for
-  scalar ABI-safe exported functions, plus `String` and `Bytes` arguments with
-  scalar returns. Same-module record and variant values can cross exported
-  typed `def` boundaries as opaque handles.
+  scalar ABI-safe exported functions, plus `String` and `Bytes` arguments and
+  returns. Same-module record and variant values can cross exported typed
+  `def` boundaries as opaque handles.
 
 ### Milestone 4: First Real Module
 
diff --git a/lib/jerboa/typed/rust.ss b/lib/jerboa/typed/rust.ss
index f0c0a0d..e2febf6 100644
--- a/lib/jerboa/typed/rust.ss
+++ b/lib/jerboa/typed/rust.ss
@@ -318,9 +318,15 @@
              (lookup-name type (*rust-variant-env*)))
          #t))
 
+  (def (abi-return-buffer-type? type)
+    (and (symbol? type)
+         (memq type '(String Bytes))
+         #t))
+
   (def (abi-safe-return-type? type)
     (or (abi-safe-type? type)
-        (abi-handle-type? type)))
+        (abi-handle-type? type)
+        (abi-return-buffer-type? type)))
 
   (def (abi-safe-param-type? type)
     (or (abi-safe-type? type)
@@ -420,6 +426,34 @@
     (write-line port 0 "}")
     (newline port))
 
+  (def (emit-byte-buffer-runtime port)
+    (write-line port 0 "fn jt_return_bytes(bytes: Vec<u8>, out_ptr: *mut *mut u8, out_len: *mut usize) -> bool {")
+    (write-line port 1 "if out_ptr.is_null() || out_len.is_null() {")
+    (write-line port 2 "return false;")
+    (write-line port 1 "}")
+    (write-line port 1 "let mut boxed = bytes.into_boxed_slice();")
+    (write-line port 1 "let len = boxed.len();")
+    (write-line port 1 "let ptr = boxed.as_mut_ptr();")
+    (write-line port 1 "std::mem::forget(boxed);")
+    (write-line port 1 "// unsafe: out pointers are provided by the generated Jerboa wrapper.")
+    (write-line port 1 "unsafe {")
+    (write-line port 2 "*out_ptr = ptr;")
+    (write-line port 2 "*out_len = len;")
+    (write-line port 1 "}")
+    (write-line port 1 "true")
+    (write-line port 0 "}")
+    (newline port)
+    (write-line port 0 "#[unsafe(no_mangle)]")
+    (write-line port 0 "pub extern \"C\" fn jt_byte_buffer_free(ptr: *mut u8, len: usize) {")
+    (write-line port 1 "if !ptr.is_null() {")
+    (write-line port 2 "// unsafe: ptr/len must be a buffer returned by jt_return_bytes.")
+    (write-line port 2 "unsafe {")
+    (write-line port 3 "drop(std::boxed::Box::from_raw(std::slice::from_raw_parts_mut(ptr, len)));")
+    (write-line port 2 "}")
+    (write-line port 1 "}")
+    (write-line port 0 "}")
+    (newline port))
+
   (def (abi-def-uses-handles? def)
     (or (abi-handle-type? (typed-def-return-type def))
         (let loop ([params (typed-def-params def)])
@@ -440,6 +474,38 @@
         [else
          (loop (cdr decls))])))
 
+  (def (module-needs-byte-buffer-runtime? module)
+    (let loop ([decls (typed-module-declarations module)])
+      (cond
+        [(null? decls) #f]
+        [(and (typed-def? (car decls))
+              (exported-def? module (car decls))
+              (abi-safe-def? (car decls))
+              (abi-return-buffer-type? (typed-def-return-type (car decls))))
+         #t]
+        [else
+         (loop (cdr decls))])))
+
+  (def (modules-need-handle-registry? modules)
+    (let loop ([rest modules])
+      (cond
+        [(null? rest) #f]
+        [(module-needs-handle-registry? (car rest)) #t]
+        [else (loop (cdr rest))])))
+
+  (def (modules-need-byte-buffer-runtime? modules)
+    (let loop ([rest modules])
+      (cond
+        [(null? rest) #f]
+        [(module-needs-byte-buffer-runtime? (car rest)) #t]
+        [else (loop (cdr rest))])))
+
+  (def (emit-runtime-helpers modules port)
+    (when (modules-need-handle-registry? modules)
+      (emit-handle-registry port))
+    (when (modules-need-byte-buffer-runtime? modules)
+      (emit-byte-buffer-runtime port)))
+
   (def (emit-abi-param-conversion param port)
     (let ([name (rust-symbol-name (typed-param-name param))])
       (case (typed-param-type param)
@@ -494,6 +560,15 @@
         [(abi-handle-type? return-type) (string-append "jt_store_handle(" call ")")]
         [else call])))
 
+  (def (abi-return-buffer-expression def call)
+    (case (typed-def-return-type def)
+      [(String)
+       (string-append "jt_return_bytes(" call ".into_bytes(), out_ptr, out_len)")]
+      [(Bytes)
+       (string-append "jt_return_bytes(" call ", out_ptr, out_len)")]
+      [else (error 'typed-rust "unsupported ABI return buffer type"
+              (typed-def-return-type def))]))
+
   (def (abi-default-expression type)
     (case type
       [(Unit) "()"]
@@ -511,6 +586,8 @@
   (def (emit-abi-wrapper module def port)
     (when (abi-safe-def? def)
       (let* ([params (typed-def-params def)]
+             [return-type (typed-def-return-type def)]
+             [return-buffer? (abi-return-buffer-type? return-type)]
              [param-names (map (lambda (param)
                                  (rust-symbol-name (typed-param-name param)))
                                params)]
@@ -519,16 +596,24 @@
                 (rust-symbol-name (typed-def-name def))
                 "("
                 (join-strings param-names ", ")
-                ")")])
+                ")")]
+             [abi-params
+              (append
+                (append-map emit-abi-param params)
+                (if return-buffer?
+                  '("out_ptr: *mut *mut u8" "out_len: *mut usize")
+                  '()))])
         (write-line port 0 "#[unsafe(no_mangle)]")
         (write-line port 0
           (string-append
             "pub extern \"C\" fn "
             (abi-wrapper-name module def)
             "("
-            (join-strings (append-map emit-abi-param params) ", ")
+            (join-strings abi-params ", ")
             ") -> "
-            (abi-rust-type (typed-def-return-type def))
+            (if return-buffer?
+              "bool"
+              (abi-rust-type return-type))
             " {"))
         (write-line port 1
           "match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {")
@@ -536,13 +621,18 @@
           (lambda (param)
             (emit-abi-param-conversion param port))
           params)
-        (write-line port 2 (abi-return-expression def call))
+        (write-line port 2
+          (if return-buffer?
+            (abi-return-buffer-expression def call)
+            (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))
+            (if return-buffer?
+              "false"
+              (abi-default-expression return-type))
             ","))
         (write-line port 1 "}")
         (write-line port 0 "}")
@@ -767,6 +857,20 @@
       [(char? expr) (rust-char-literal expr)]
       [(string? expr)
        (string-append "String::from(" (rust-string-literal expr) ")")]
+      [(bytevector? expr)
+       (let ([len (bytevector-length expr)])
+         (string-append
+           "vec!["
+           (let loop ([i 0] [out '()])
+             (if (= i len)
+               (join-strings (reverse out) ", ")
+               (loop (+ i 1)
+                     (cons
+                       (string-append
+                         (number->string (bytevector-u8-ref expr i))
+                         "u8")
+                       out))))
+           "]"))]
       [(integer? expr)
        (if (>= expr 0)
          (string-append (number->string expr) "u64")
@@ -829,8 +933,6 @@
     (newline port))
 
   (def (emit-module-declarations module port)
-    (when (module-needs-handle-registry? module)
-      (emit-handle-registry port))
     (for-each
       (lambda (decl)
         (emit-declaration decl port))
@@ -844,6 +946,7 @@
 
   (def (emit-module module port)
     (emit-rust-header port)
+    (emit-runtime-helpers (list module) port)
     (emit-module-declarations module port))
 
   (def (typed-module->rust-string module)
@@ -875,6 +978,7 @@
         (emit-to-string
           (lambda (port)
             (emit-rust-header port)
+            (emit-runtime-helpers modules port)
             (for-each
               (lambda (module)
                 (emit-module-declarations module port))
diff --git a/lib/jerboa/typed/wrapper.ss b/lib/jerboa/typed/wrapper.ss
index 5ef8bc8..a099c9c 100644
--- a/lib/jerboa/typed/wrapper.ss
+++ b/lib/jerboa/typed/wrapper.ss
@@ -109,9 +109,15 @@
          (memq type '(Unit Bool Char Int Nat Fixnum Float))
          #t))
 
+  (def (abi-wrapper-return-buffer-type? type)
+    (and (symbol? type)
+         (memq type '(String Bytes))
+         #t))
+
   (def (wrapper-safe-return-type? module type)
     (or (abi-wrapper-scalar-type? type)
-        (module-handle-type? module type)))
+        (module-handle-type? module type)
+        (abi-wrapper-return-buffer-type? type)))
 
   (def (wrapper-safe-param-type? module type)
     (or (abi-wrapper-scalar-type? type)
@@ -128,9 +134,10 @@
              [else #f]))))
 
   (def (abi-chez-return-type module type)
-    (if (module-handle-type? module type)
-      'unsigned-64
-      (abi-chez-type type)))
+    (cond
+      [(abi-wrapper-return-buffer-type? type) 'boolean]
+      [(module-handle-type? module type) 'unsigned-64]
+      [else (abi-chez-type type)]))
 
   (def (abi-chez-argument-types module type)
     (case type
@@ -150,6 +157,13 @@
          (loop (cdr rest) (cons (car rest) out))]
         [else (loop (cdr rest) out)])))
 
+  (def (module-needs-return-buffer? module)
+    (let loop ([defs (wrapper-defs module)])
+      (cond
+        [(null? defs) #f]
+        [(abi-wrapper-return-buffer-type? (typed-def-return-type (car defs))) #t]
+        [else (loop (cdr defs))])))
+
   (def (ffi-binding-name def)
     (string->symbol
       (string-append "%" (rust-symbol-name (typed-def-name def)))))
@@ -227,24 +241,38 @@
            (list (string-append "(%typed-rust-handle-id " name ")"))
            (list name))])))
 
-  (def (wrapper-call-expression module def)
+  (def (wrapper-call-expression* module def extra-args)
     (string-append
       "("
       (datum->code (ffi-binding-name def))
-      (let ([args (append-map
-                    (lambda (param)
-                      (wrapper-argument-expressions module param))
-                    (typed-def-params def))])
+      (let ([args (append
+                    (append-map
+                      (lambda (param)
+                        (wrapper-argument-expressions module param))
+                      (typed-def-params def))
+                    extra-args)])
         (if (null? args)
           ""
           (string-append " " (join-strings args " "))))
       ")"))
 
+  (def (wrapper-call-expression module def)
+    (wrapper-call-expression* module def '()))
+
   (def (wrapper-return-expression module def)
     (let ([call (wrapper-call-expression module def)]
           [return-type (typed-def-return-type def)])
       (cond
         [(eq? return-type 'Char) (string-append "(integer->char " call ")")]
+        [(abi-wrapper-return-buffer-type? return-type)
+         (string-append
+           (if (eq? return-type 'String)
+             "(%typed-rust-return-string "
+             "(%typed-rust-return-bytes ")
+           (quoted-symbol-code (typed-def-name def))
+           " (lambda (%out_ptr %out_len) "
+           (wrapper-call-expression* module def '("%out_ptr" "%out_len"))
+           "))")]
         [(module-handle-type? module return-type)
          (string-append
            "(%typed-rust-make-handle "
@@ -265,11 +293,15 @@
         (datum->code (abi-wrapper-name module def))
         " ("
         (join-strings
-          (append-map
-            (lambda (param)
-              (map datum->code
-                   (abi-chez-argument-types module (typed-param-type param))))
-            (typed-def-params def))
+          (append
+            (append-map
+              (lambda (param)
+                (map datum->code
+                     (abi-chez-argument-types module (typed-param-type param))))
+              (typed-def-params def))
+            (if (abi-wrapper-return-buffer-type? (typed-def-return-type def))
+              '("void*" "void*")
+              '()))
           " ")
         ") "
         (datum->code (abi-chez-return-type module (typed-def-return-type def)))
@@ -324,10 +356,14 @@
       (write-line port 0 ")")
       (newline port)))
 
-  (def (emit-wrapper-header port)
+  (def (emit-wrapper-header needs-return-buffer? port)
     (write-line port 0 ";; Generated by Jerboa's typed wrapper backend. Do not edit.")
     (write-line port 0 "(import (jerboa prelude)")
-    (write-line port 0 "        (only (chezscheme) foreign-procedure getenv load-shared-object))")
+    (if needs-return-buffer?
+      (begin
+        (write-line port 0 "        (only (chezscheme) foreign-procedure getenv load-shared-object")
+        (write-line port 0 "              foreign-alloc foreign-free foreign-ref foreign-set! foreign-sizeof))"))
+      (write-line port 0 "        (only (chezscheme) foreign-procedure getenv load-shared-object))"))
     (newline port)
     (write-line port 0 "(def %typed-rust-library-path (getenv \"JERBOA_TYPED_RUST_LIB\"))")
     (write-line port 0 "(when %typed-rust-library-path")
@@ -359,10 +395,50 @@
     (newline port)
     (write-line port 0 "(def (%typed-rust-handle-id value)")
     (write-line port 1 "(vector-ref value 2))")
-    (newline port))
+    (if needs-return-buffer?
+      (begin
+      (newline port)
+      (write-line port 0 "(def %typed-rust-byte-buffer-free")
+      (write-line port 1 "(foreign-procedure \"jt_byte_buffer_free\" (void* size_t) void))")
+      (newline port)
+      (write-line port 0 "(def (%typed-rust-copy-byte-buffer ptr len)")
+      (write-line port 1 "(let ([out (make-bytevector len)])")
+      (write-line port 2 "(let loop ([i 0])")
+      (write-line port 3 "(when (< i len)")
+      (write-line port 4 "(bytevector-u8-set! out i (foreign-ref 'unsigned-8 ptr i))")
+      (write-line port 4 "(loop (+ i 1))))")
+      (write-line port 2 "out))")
+      (newline port)
+      (write-line port 0 "(def (%typed-rust-take-byte-buffer ptr len)")
+      (write-line port 1 "(dynamic-wind")
+      (write-line port 2 "(lambda () #f)")
+      (write-line port 2 "(lambda () (%typed-rust-copy-byte-buffer ptr len))")
+      (write-line port 2 "(lambda () (%typed-rust-byte-buffer-free ptr len))))")
+      (newline port)
+      (write-line port 0 "(def (%typed-rust-return-bytes who thunk)")
+      (write-line port 1 "(let ([ptr-box (foreign-alloc (foreign-sizeof 'void*))]")
+      (write-line port 2 "[len-box (foreign-alloc (foreign-sizeof 'size_t))])")
+      (write-line port 2 "(dynamic-wind")
+      (write-line port 3 "(lambda () #f)")
+      (write-line port 3 "(lambda ()")
+      (write-line port 4 "(foreign-set! 'void* ptr-box 0 0)")
+      (write-line port 4 "(foreign-set! 'size_t len-box 0 0)")
+      (write-line port 4 "(unless (thunk ptr-box len-box)")
+      (write-line port 5 "(error who \"typed Rust call failed\"))")
+      (write-line port 4 "(%typed-rust-take-byte-buffer")
+      (write-line port 5 "(foreign-ref 'void* ptr-box 0)")
+      (write-line port 5 "(foreign-ref 'size_t len-box 0)))")
+      (write-line port 3 "(lambda ()")
+      (write-line port 4 "(foreign-free ptr-box)")
+      (write-line port 4 "(foreign-free len-box)))))")
+      (newline port)
+      (write-line port 0 "(def (%typed-rust-return-string who thunk)")
+      (write-line port 1 "(utf8->string (%typed-rust-return-bytes who thunk)))")
+      (newline port))
+      (newline port)))
 
   (def (emit-module-wrapper module port)
-    (emit-wrapper-header port)
+    (emit-wrapper-header (module-needs-return-buffer? module) port)
     (for-each
       (lambda (def)
         (emit-ffi-binding module def port))
diff --git a/tests/fixtures/typed/rust-basic.ss b/tests/fixtures/typed/rust-basic.ss
index 66f4b91..bd8e7be 100644
--- a/tests/fixtures/typed/rust-basic.ss
+++ b/tests/fixtures/typed/rust-basic.ss
@@ -1,6 +1,6 @@
 (typed-library (sample typed rust-basic)
-  (export zero add-one positive? choose greeting double-add text-length bytes-length
-          make-box box-value make-some token-size)
+  (export zero add-one positive? choose greeting echo-text double-add text-length
+          bytes-length echo-bytes make-box box-value make-some token-size)
 
   (record Box
     ((value : Nat)))
@@ -24,6 +24,9 @@
   (def (greeting) : String
     "hello")
 
+  (def (echo-text (text : String)) : String
+    text)
+
   (def (double-add (x : Nat)) : Nat
     (let ((y (+ x x)))
       (+ y 1)))
@@ -34,6 +37,9 @@
   (def (bytes-length (data : Bytes)) : Nat
     (bytevector-length data))
 
+  (def (echo-bytes (data : Bytes)) : Bytes
+    data)
+
   (def (make-box (value : Nat)) : Box
     (make-Box value))
 
diff --git a/tests/test-typed-rust.ss b/tests/test-typed-rust.ss
index 97e5075..13c72ee 100644
--- a/tests/test-typed-rust.ss
+++ b/tests/test-typed-rust.ss
@@ -83,6 +83,19 @@
 (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    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 return-form
+  '(typed-library (sample typed return-values)
+     (export greeting echo-text echo-bytes)
+     (def (greeting) : String
+       "hello")
+     (def (echo-text (text : String)) : String
+       text)
+     (def (echo-bytes (data : Bytes)) : Bytes
+       data)))
+
+(define return-rust
+  (typed-library-form->rust-string return-form))
+
 (define ops-form
   '(typed-library (sample typed ops)
      (export)
@@ -154,6 +167,33 @@
   (typed-library-form->rust-string bytes-form)
   bytes-rust)
 
+(test "rust emits return byte buffer runtime"
+  (and (substring? return-rust
+         "fn jt_return_bytes(bytes: Vec<u8>, out_ptr: *mut *mut u8, out_len: *mut usize) -> bool")
+       (substring? return-rust
+         "pub extern \"C\" fn jt_byte_buffer_free(ptr: *mut u8, len: usize)")
+       (substring? return-rust
+         "std::boxed::Box::from_raw(std::slice::from_raw_parts_mut(ptr, len))"))
+  #t)
+
+(test "rust emits string return ABI wrappers"
+  (and (substring? return-rust
+         "pub extern \"C\" fn jt_sample_typed_return_values_greeting(out_ptr: *mut *mut u8, out_len: *mut usize) -> bool")
+       (substring? return-rust
+         "jt_return_bytes(greeting().into_bytes(), out_ptr, out_len)")
+       (substring? return-rust
+         "pub extern \"C\" fn jt_sample_typed_return_values_echo_text(text_ptr: *const u8, text_len: usize, out_ptr: *mut *mut u8, out_len: *mut usize) -> bool")
+       (substring? return-rust
+         "jt_return_bytes(echo_text(text).into_bytes(), out_ptr, out_len)"))
+  #t)
+
+(test "rust emits bytes return ABI wrappers"
+  (and (substring? return-rust
+         "pub extern \"C\" fn jt_sample_typed_return_values_echo_bytes(data_ptr: *const u8, data_len: usize, out_ptr: *mut *mut u8, out_len: *mut usize) -> bool")
+       (substring? return-rust
+         "jt_return_bytes(echo_bytes(data), out_ptr, out_len)"))
+  #t)
+
 (test "rust lowers record and variant operations"
   (typed-library-form->rust-string ops-form)
   ops-rust)
diff --git a/tests/test-typed-wrapper-e2e.ss b/tests/test-typed-wrapper-e2e.ss
index 6dfc6f7..c533890 100644
--- a/tests/test-typed-wrapper-e2e.ss
+++ b/tests/test-typed-wrapper-e2e.ss
@@ -29,15 +29,33 @@
     (thunk)
     #f))
 
+(define (bytevector-equal? a b)
+  (and (= (bytevector-length a) (bytevector-length b))
+       (let loop ([i 0])
+         (cond
+           [(= i (bytevector-length a)) #t]
+           [(= (bytevector-u8-ref a i) (bytevector-u8-ref b i))
+            (loop (+ i 1))]
+           [else #f]))))
+
+(define sample-bytes (make-bytevector 4 0))
+(bytevector-u8-set! sample-bytes 0 1)
+(bytevector-u8-set! sample-bytes 1 2)
+(bytevector-u8-set! sample-bytes 2 3)
+(bytevector-u8-set! sample-bytes 3 255)
+
 (printf "--- Typed Jerboa wrapper FFI smoke ---~%")
 
 (check "zero" (= (zero) 0))
 (check "add-one" (= (add-one 41) 42))
 (check "positive?" (positive? 9))
 (check "choose" (= (choose #t) 1))
+(check "greeting" (string=? (greeting) "hello"))
+(check "echo-text" (string=? (echo-text "sample") "sample"))
 (check "double-add" (= (double-add 20) 41))
 (check "text-length" (= (text-length "hello") 5))
 (check "bytes-length" (= (bytes-length (make-bytevector 7 0)) 7))
+(check "echo-bytes" (bytevector-equal? (echo-bytes sample-bytes) sample-bytes))
 (check "record handle round trip"
   (= (box-value (make-box 17)) 17))
 (check "variant handle round trip"
@@ -48,8 +66,12 @@
   (raises? (lambda () (choose 1))))
 (check "text-length rejects non-String"
   (raises? (lambda () (text-length (make-bytevector 3 0)))))
+(check "echo-text rejects non-String"
+  (raises? (lambda () (echo-text (make-bytevector 3 0)))))
 (check "bytes-length rejects non-Bytes"
   (raises? (lambda () (bytes-length "not bytes"))))
+(check "echo-bytes rejects non-Bytes"
+  (raises? (lambda () (echo-bytes "not bytes"))))
 (check "record handle rejects wrong type"
   (raises? (lambda () (box-value (make-some 7)))))
 (check "variant handle rejects wrong type"
diff --git a/tests/test-typed-wrappers.ss b/tests/test-typed-wrappers.ss
index 2640b22..3c4ec04 100644
--- a/tests/test-typed-wrappers.ss
+++ b/tests/test-typed-wrappers.ss
@@ -60,6 +60,19 @@
 (define bytes-wrapper
   (typed-library-form->jerboa-wrapper-string bytes-form))
 
+(define return-form
+  '(typed-library (sample typed return-values)
+     (export greeting echo-text echo-bytes)
+     (def (greeting) : String
+       "hello")
+     (def (echo-text (text : String)) : String
+       text)
+     (def (echo-bytes (data : Bytes)) : Bytes
+       data)))
+
+(define return-wrapper
+  (typed-library-form->jerboa-wrapper-string return-form))
+
 (define handle-form
   '(typed-library (sample typed handles)
      (export make-box box-value make-some token-size)
@@ -134,6 +147,40 @@
     "(%bytes_length data (bytevector-length data))")
   #t)
 
+(test "wrapper emits return byte buffer helpers"
+  (and (substring? return-wrapper
+         "(only (chezscheme) foreign-procedure getenv load-shared-object")
+       (substring? return-wrapper
+         "foreign-alloc foreign-free foreign-ref foreign-set! foreign-sizeof")
+       (substring? return-wrapper
+         "(foreign-procedure \"jt_byte_buffer_free\" (void* size_t) void)")
+       (substring? return-wrapper
+         "(def (%typed-rust-return-bytes who thunk)")
+       (substring? return-wrapper
+         "(def (%typed-rust-return-string who thunk)"))
+  #t)
+
+(test "wrapper binds String returns as out buffers"
+  (and (substring? return-wrapper
+         "(foreign-procedure \"jt_sample_typed_return_values_greeting\" (void* void*) boolean)")
+       (substring? return-wrapper
+         "(foreign-procedure \"jt_sample_typed_return_values_echo_text\" (u8* size_t void* void*) boolean)"))
+  #t)
+
+(test "wrapper converts String returns"
+  (and (substring? return-wrapper
+         "(%typed-rust-return-string 'greeting (lambda (%out_ptr %out_len) (%greeting %out_ptr %out_len)))")
+       (substring? return-wrapper
+         "(%typed-rust-return-string 'echo-text (lambda (%out_ptr %out_len) (%echo_text %text_bytes (bytevector-length %text_bytes) %out_ptr %out_len)))"))
+  #t)
+
+(test "wrapper binds and converts Bytes returns"
+  (and (substring? return-wrapper
+         "(foreign-procedure \"jt_sample_typed_return_values_echo_bytes\" (u8* size_t void* void*) boolean)")
+       (substring? return-wrapper
+         "(%typed-rust-return-bytes 'echo-bytes (lambda (%out_ptr %out_len) (%echo_bytes data (bytevector-length data) %out_ptr %out_len)))"))
+  #t)
+
 (test "wrapper returns record and variant handles"
   (and (substring? handle-wrapper
          "(%typed-rust-make-handle 'Box (%make_box value))")