weaveffi/abi/convert.rs
1//! Marshalling helpers that bridge Rust values and the C ABI's `(ptr, len)`
2//! slots.
3//!
4//! These functions are the audited home of the pointer operations a WeaveFFI
5//! producer performs on strings, bytes, and value buffers. The
6//! `#[weaveffi::module]` expansion wires the generated thunks to them:
7//!
8//! * **lift** functions read a borrowed `(ptr, len)` parameter. They never
9//! take ownership of caller memory; the borrowing variants
10//! ([`lift_str`], [`lift_byte_slice`]) don't even copy.
11//! * **lower** functions hand an owned value to the consumer as a
12//! producer-allocated `(ptr, len)` run, which the consumer releases with
13//! `{prefix}_free_bytes` (see [`free_bytes`]).
14//! * [`alloc`] is the body of `{prefix}_alloc`: a run the consumer fills and
15//! either frees with `{prefix}_free_bytes` or hands to the producer (a
16//! callback's string, bytes, or buffer return), which adopts it with
17//! [`adopt_bytes`]. Every run is a `Box<[u8]>` allocation, so one release
18//! function covers both directions.
19//!
20//! Strings, bytes, and serialized value buffers all share this one
21//! representation: a string is its UTF-8 bytes, never NUL-terminated, so an
22//! interior NUL round-trips intact. A null pointer is valid only with a
23//! length of `0` and denotes the empty run.
24
25/// Borrow a `(ptr, len)` parameter as a byte slice for a caller-chosen
26/// lifetime, without copying.
27///
28/// Returns `None` when `ptr` is null but `len` isn't `0`, which the thunk
29/// reports as a marshalling failure.
30///
31/// # Safety
32///
33/// When `ptr` is non-null it must point to `len` initialized bytes that stay
34/// valid and unmodified for the whole lifetime `'a`. Generated thunks bound
35/// `'a` by the call, matching the contract that parameters are borrowed for
36/// the call's duration.
37#[must_use]
38pub unsafe fn lift_byte_slice<'a>(ptr: *const u8, len: usize) -> Option<&'a [u8]> {
39 if ptr.is_null() {
40 return (len == 0).then_some(&[][..]);
41 }
42 // SAFETY: the caller guarantees `ptr` covers `len` bytes valid for `'a`.
43 Some(unsafe { std::slice::from_raw_parts(ptr, len) })
44}
45
46/// Copy a `(ptr, len)` parameter into an owned `Vec<u8>`.
47///
48/// Returns `None` when `ptr` is null but `len` isn't `0`.
49///
50/// # Safety
51///
52/// Same contract as [`lift_byte_slice`], for the duration of the call.
53#[must_use]
54pub unsafe fn lift_bytes(ptr: *const u8, len: usize) -> Option<Vec<u8>> {
55 // SAFETY: forwarded from the caller.
56 unsafe { lift_byte_slice(ptr, len) }.map(<[u8]>::to_vec)
57}
58
59/// Borrow a `(ptr, len)` string parameter as a `&str`, validating UTF-8,
60/// without copying.
61///
62/// Returns `None` when `ptr` is null but `len` isn't `0`, or when the bytes
63/// aren't valid UTF-8.
64///
65/// # Safety
66///
67/// Same contract as [`lift_byte_slice`].
68#[must_use]
69pub unsafe fn lift_str<'a>(ptr: *const u8, len: usize) -> Option<&'a str> {
70 // SAFETY: forwarded from the caller.
71 std::str::from_utf8(unsafe { lift_byte_slice(ptr, len) }?).ok()
72}
73
74/// Copy a `(ptr, len)` string parameter into an owned `String`, validating
75/// UTF-8.
76///
77/// Returns `None` when `ptr` is null but `len` isn't `0`, or when the bytes
78/// aren't valid UTF-8.
79///
80/// # Safety
81///
82/// Same contract as [`lift_byte_slice`], for the duration of the call.
83#[must_use]
84pub unsafe fn lift_string(ptr: *const u8, len: usize) -> Option<String> {
85 // SAFETY: forwarded from the caller.
86 unsafe { lift_str(ptr, len) }.map(str::to_owned)
87}
88
89/// Turn an owned byte vector into the producer-allocated `(ptr, len)` run a
90/// return slot or async result carries, to be released with
91/// [`free_bytes`]. An empty vector yields `(null, 0)` and allocates nothing.
92///
93/// When the vector's capacity equals its length (as it does for every
94/// encoding [`encode_value`](crate::abi::encode_value) produces) the conversion
95/// reuses the allocation as is; otherwise it's shrunk to fit first.
96#[must_use]
97pub fn bytes_into_raw(data: Vec<u8>) -> (*const u8, usize) {
98 if data.is_empty() {
99 return (std::ptr::null(), 0);
100 }
101 let len = data.len();
102 let ptr = Box::into_raw(data.into_boxed_slice())
103 .cast::<u8>()
104 .cast_const();
105 crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, 1);
106 (ptr, len)
107}
108
109/// Lower an owned byte vector into a return value plus the trailing
110/// `size_t* out_len` slot. See [`bytes_into_raw`].
111///
112/// # Safety
113///
114/// `out_len` must be null or point to a writable `usize`.
115pub unsafe fn lower_bytes(data: Vec<u8>, out_len: *mut usize) -> *const u8 {
116 let (ptr, len) = bytes_into_raw(data);
117 if !out_len.is_null() {
118 // SAFETY: the caller guarantees `out_len` is writable when non-null.
119 unsafe { *out_len = len };
120 }
121 ptr
122}
123
124/// Lower an owned string as its UTF-8 bytes, exactly like [`lower_bytes`].
125///
126/// # Safety
127///
128/// Same contract as [`lower_bytes`].
129pub unsafe fn lower_string(s: String, out_len: *mut usize) -> *const u8 {
130 // SAFETY: forwarded from the caller.
131 unsafe { lower_bytes(s.into_bytes(), out_len) }
132}
133
134/// The body of `{prefix}_alloc`: allocate a zero-filled run of `len` bytes
135/// the consumer fills, released with [`free_bytes`] (the same `len`) or
136/// handed to the producer, which adopts it. A `len` of `0` returns null,
137/// which is the empty run.
138#[must_use]
139pub fn alloc(len: usize) -> *mut u8 {
140 if len == 0 {
141 return std::ptr::null_mut();
142 }
143 crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, 1);
144 Box::into_raw(vec![0u8; len].into_boxed_slice()).cast::<u8>()
145}
146
147/// Adopt a run from [`alloc`] (or one this runtime returned) as an owned
148/// vector. Null with a length of `0` is the empty run; null with any other
149/// length yields `None`.
150///
151/// # Safety
152///
153/// `ptr` must be null or a run of exactly `len` bytes from [`alloc`] or
154/// [`bytes_into_raw`] that nothing else releases or uses afterward.
155#[must_use]
156pub unsafe fn adopt_bytes(ptr: *mut u8, len: usize) -> Option<Vec<u8>> {
157 if ptr.is_null() {
158 return (len == 0).then(Vec::new);
159 }
160 if len == 0 {
161 return Some(Vec::new());
162 }
163 crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, -1);
164 // SAFETY: the caller guarantees `ptr` is a `Box<[u8]>` of `len` bytes
165 // transferred to us.
166 Some(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) }.into_vec())
167}
168
169/// The body of `{prefix}_free_bytes`: release a run returned by this runtime
170/// (a string, bytes, or a value buffer) or allocated with [`alloc`]. A null
171/// `ptr` or a `len` of `0` is a no-op.
172///
173/// # Safety
174///
175/// `ptr` must be null or a pointer this runtime returned together with
176/// `len` (or [`alloc`] returned for `len`), released exactly once.
177pub unsafe fn free_bytes(ptr: *mut u8, len: usize) {
178 if ptr.is_null() || len == 0 {
179 return;
180 }
181 crate::abi::leak::track(crate::abi::leak::ALLOCATIONS, -1);
182 // SAFETY: `bytes_into_raw` produced `ptr` from a `Box<[u8]>` of `len`
183 // bytes, and the caller hands it back exactly once.
184 drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) });
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn bytes_roundtrip() {
193 let mut len = 0usize;
194 let ptr = unsafe { lower_bytes(vec![1u8, 2, 3, 4], &mut len) };
195 assert_eq!(len, 4);
196 assert_eq!(unsafe { lift_bytes(ptr, len) }, Some(vec![1, 2, 3, 4]));
197 unsafe { free_bytes(ptr.cast_mut(), len) };
198 }
199
200 #[test]
201 fn empty_is_null_and_null_is_empty() {
202 let mut len = 99usize;
203 let ptr = unsafe { lower_bytes(Vec::new(), &mut len) };
204 assert!(ptr.is_null());
205 assert_eq!(len, 0);
206 assert_eq!(unsafe { lift_str(std::ptr::null(), 0) }, Some(""));
207 assert_eq!(unsafe { lift_bytes(std::ptr::null(), 0) }, Some(vec![]));
208 unsafe { free_bytes(std::ptr::null_mut(), 0) };
209 }
210
211 #[test]
212 fn null_with_a_length_is_rejected() {
213 assert!(unsafe { lift_byte_slice(std::ptr::null(), 3) }.is_none());
214 assert!(unsafe { lift_string(std::ptr::null(), 1) }.is_none());
215 }
216
217 #[test]
218 fn strings_borrow_and_keep_interior_nul() {
219 let text = "a\0b \u{1F980}";
220 let s = unsafe { lift_str(text.as_ptr(), text.len()) }.unwrap();
221 assert!(std::ptr::eq(s.as_ptr(), text.as_ptr()));
222 assert_eq!(s, text);
223
224 let mut len = 0usize;
225 let ptr = unsafe { lower_string(text.to_string(), &mut len) };
226 assert_eq!(unsafe { lift_string(ptr, len) }.as_deref(), Some(text));
227 unsafe { free_bytes(ptr.cast_mut(), len) };
228 }
229
230 #[test]
231 fn alloc_runs_free_and_adopt() {
232 assert!(alloc(0).is_null());
233 let p = alloc(4);
234 assert_eq!(unsafe { std::slice::from_raw_parts(p, 4) }, [0; 4]);
235 unsafe { free_bytes(p, 4) };
236 let p = alloc(2);
237 unsafe { *p = 9 };
238 assert_eq!(unsafe { adopt_bytes(p, 2) }, Some(vec![9, 0]));
239 assert_eq!(
240 unsafe { adopt_bytes(std::ptr::null_mut(), 0) },
241 Some(vec![])
242 );
243 assert_eq!(unsafe { adopt_bytes(std::ptr::null_mut(), 1) }, None);
244 }
245
246 #[test]
247 fn invalid_utf8_is_rejected() {
248 let bad = [0xFFu8, 0xFE];
249 assert!(unsafe { lift_str(bad.as_ptr(), bad.len()) }.is_none());
250 }
251}