Skip to main content

shape_jit/ffi/
v2_struct.rs

1//! v2 Typed Struct FFI Functions for JIT
2//!
3//! Provides allocation, field access, and refcounting for v2 typed structs.
4//! These structs have a fixed layout with a HeapHeader (8 bytes) followed by
5//! fields at compile-time-known offsets:
6//!
7//! ```text
8//! +------------------+------------------+------------------+-----
9//! | HeapHeader       | field[0]         | field[1]         | ...
10//! | (8 bytes)        | (type-dependent) | (type-dependent) |
11//! +------------------+------------------+------------------+-----
12//!   offset 0           offset 8           offset 8+sizeof(f0)
13//! ```
14//!
15//! HeapHeader layout (8 bytes):
16//! - offset 0: refcount (u32) - initialized to 1
17//! - offset 4: kind (u16) - set to HK_V2_TYPED_STRUCT
18//! - offset 6: flags (u8) - reserved
19//! - offset 7: padding (u8)
20//!
21//! Field access uses raw pointer arithmetic with byte offsets known at
22//! compile time, giving O(1) access with no schema lookup.
23
24use std::alloc::{alloc_zeroed, dealloc, Layout};
25
26/// Heap kind tag for v2 typed structs.
27/// Uses a JIT-private range (132+) to avoid collision with existing HK_ constants.
28pub const HK_V2_TYPED_STRUCT: u16 = 132;
29
30/// Byte offset of the `kind` field within the HeapHeader.
31const HEADER_KIND_OFFSET: usize = 4;
32
33/// Allocate a v2 typed struct with the given total size (including header).
34///
35/// Sets refcount=1 and kind=HK_V2_TYPED_STRUCT. All field bytes are zeroed.
36///
37/// # Arguments
38/// * `total_size` - Total allocation size in bytes (header + fields). Must be >= 8.
39///
40/// # Returns
41/// Pointer to the allocated struct, or null on allocation failure.
42#[unsafe(no_mangle)]
43pub extern "C" fn jit_v2_struct_alloc(total_size: u32) -> *mut u8 {
44    let size = total_size as usize;
45    if size < 8 {
46        return std::ptr::null_mut();
47    }
48
49    let layout = match Layout::from_size_align(size, 8) {
50        Ok(l) => l,
51        Err(_) => return std::ptr::null_mut(),
52    };
53
54    let ptr = unsafe { alloc_zeroed(layout) };
55    if ptr.is_null() {
56        return ptr;
57    }
58
59    unsafe {
60        // Write refcount = 1 at offset 0
61        (ptr as *mut u32).write(1);
62        // Write kind = HK_V2_TYPED_STRUCT at offset 4
63        (ptr.add(HEADER_KIND_OFFSET) as *mut u16).write(HK_V2_TYPED_STRUCT);
64    }
65
66    ptr
67}
68
69// ---------------------------------------------------------------------------
70// f64 field access
71// ---------------------------------------------------------------------------
72
73/// Read an f64 field at the given byte offset.
74///
75/// # Safety
76/// `ptr` must be a valid v2 struct pointer. `offset` must be aligned to 8 and
77/// within the allocated region.
78#[unsafe(no_mangle)]
79pub extern "C" fn jit_v2_struct_get_f64(ptr: *const u8, offset: u32) -> f64 {
80    unsafe { *(ptr.add(offset as usize) as *const f64) }
81}
82
83/// Write an f64 field at the given byte offset.
84#[unsafe(no_mangle)]
85pub extern "C" fn jit_v2_struct_set_f64(ptr: *mut u8, offset: u32, val: f64) {
86    unsafe {
87        *(ptr.add(offset as usize) as *mut f64) = val;
88    }
89}
90
91// ---------------------------------------------------------------------------
92// i64 field access
93// ---------------------------------------------------------------------------
94
95/// Read an i64 field at the given byte offset.
96#[unsafe(no_mangle)]
97pub extern "C" fn jit_v2_struct_get_i64(ptr: *const u8, offset: u32) -> i64 {
98    unsafe { *(ptr.add(offset as usize) as *const i64) }
99}
100
101/// Write an i64 field at the given byte offset.
102#[unsafe(no_mangle)]
103pub extern "C" fn jit_v2_struct_set_i64(ptr: *mut u8, offset: u32, val: i64) {
104    unsafe {
105        *(ptr.add(offset as usize) as *mut i64) = val;
106    }
107}
108
109// ---------------------------------------------------------------------------
110// i32 field access
111// ---------------------------------------------------------------------------
112
113/// Read an i32 field at the given byte offset.
114#[unsafe(no_mangle)]
115pub extern "C" fn jit_v2_struct_get_i32(ptr: *const u8, offset: u32) -> i32 {
116    unsafe { *(ptr.add(offset as usize) as *const i32) }
117}
118
119/// Write an i32 field at the given byte offset.
120#[unsafe(no_mangle)]
121pub extern "C" fn jit_v2_struct_set_i32(ptr: *mut u8, offset: u32, val: i32) {
122    unsafe {
123        *(ptr.add(offset as usize) as *mut i32) = val;
124    }
125}
126
127// ---------------------------------------------------------------------------
128// bool (u8) field access
129// ---------------------------------------------------------------------------
130
131/// Read a bool field (stored as u8) at the given byte offset.
132#[unsafe(no_mangle)]
133pub extern "C" fn jit_v2_struct_get_bool(ptr: *const u8, offset: u32) -> u8 {
134    unsafe { *ptr.add(offset as usize) }
135}
136
137/// Write a bool field (stored as u8) at the given byte offset.
138#[unsafe(no_mangle)]
139pub extern "C" fn jit_v2_struct_set_bool(ptr: *mut u8, offset: u32, val: u8) {
140    unsafe {
141        *ptr.add(offset as usize) = val;
142    }
143}
144
145// ---------------------------------------------------------------------------
146// Pointer (usize / *mut u8) field access — for nested structs and strings
147// ---------------------------------------------------------------------------
148
149/// Read a pointer field at the given byte offset.
150#[unsafe(no_mangle)]
151pub extern "C" fn jit_v2_struct_get_ptr(ptr: *const u8, offset: u32) -> *mut u8 {
152    unsafe { *(ptr.add(offset as usize) as *const *mut u8) }
153}
154
155/// Write a pointer field at the given byte offset.
156#[unsafe(no_mangle)]
157pub extern "C" fn jit_v2_struct_set_ptr(ptr: *mut u8, offset: u32, val: *mut u8) {
158    unsafe {
159        *(ptr.add(offset as usize) as *mut *mut u8) = val;
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Refcounting
165// ---------------------------------------------------------------------------
166
167/// Increment the refcount of a v2 typed struct.
168///
169/// # Safety
170/// `ptr` must be a valid v2 struct pointer (or null, in which case this is a no-op).
171#[unsafe(no_mangle)]
172pub extern "C" fn jit_v2_struct_retain(ptr: *mut u8) {
173    if ptr.is_null() {
174        return;
175    }
176    unsafe {
177        let rc_ptr = ptr as *mut u32;
178        let rc = rc_ptr.read();
179        rc_ptr.write(rc.saturating_add(1));
180    }
181}
182
183/// Decrement the refcount of a v2 typed struct. Deallocates when it reaches 0.
184///
185/// # Arguments
186/// * `ptr` - Pointer to the struct.
187/// * `total_size` - Total allocation size (must match the size passed to `jit_v2_struct_alloc`).
188///
189/// # Safety
190/// `ptr` must be a valid v2 struct pointer (or null). `total_size` must match the
191/// original allocation size.
192#[unsafe(no_mangle)]
193pub extern "C" fn jit_v2_struct_release(ptr: *mut u8, total_size: u32) {
194    if ptr.is_null() {
195        return;
196    }
197    unsafe {
198        let rc_ptr = ptr as *mut u32;
199        let rc = rc_ptr.read();
200        if rc <= 1 {
201            // Refcount reached zero — deallocate
202            let layout = Layout::from_size_align_unchecked(total_size as usize, 8);
203            dealloc(ptr, layout);
204        } else {
205            rc_ptr.write(rc - 1);
206        }
207    }
208}
209
210/// Read the current refcount of a v2 typed struct (for testing/debugging).
211#[unsafe(no_mangle)]
212pub extern "C" fn jit_v2_struct_refcount(ptr: *const u8) -> u32 {
213    if ptr.is_null() {
214        return 0;
215    }
216    unsafe { (ptr as *const u32).read() }
217}
218
219// ---------------------------------------------------------------------------
220// Tests
221// ---------------------------------------------------------------------------
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_alloc_and_header() {
229        // Allocate a struct: HeapHeader (8) + two f64 fields (16) = 24 bytes
230        let ptr = jit_v2_struct_alloc(24);
231        assert!(!ptr.is_null());
232
233        // Refcount should be 1
234        assert_eq!(jit_v2_struct_refcount(ptr), 1);
235
236        // Kind should be HK_V2_TYPED_STRUCT
237        let kind = unsafe { (ptr.add(HEADER_KIND_OFFSET) as *const u16).read() };
238        assert_eq!(kind, HK_V2_TYPED_STRUCT);
239
240        // Clean up
241        jit_v2_struct_release(ptr, 24);
242    }
243
244    #[test]
245    fn test_f64_field_access() {
246        // type Point { x: number, y: number }
247        // offset 8: x (f64), offset 16: y (f64)
248        let ptr = jit_v2_struct_alloc(24);
249        assert!(!ptr.is_null());
250
251        // Write fields
252        jit_v2_struct_set_f64(ptr, 8, 3.14);
253        jit_v2_struct_set_f64(ptr, 16, 2.718);
254
255        // Read back
256        assert_eq!(jit_v2_struct_get_f64(ptr, 8), 3.14);
257        assert_eq!(jit_v2_struct_get_f64(ptr, 16), 2.718);
258
259        jit_v2_struct_release(ptr, 24);
260    }
261
262    #[test]
263    fn test_i64_field_access() {
264        // HeapHeader (8) + one i64 field (8) = 16 bytes
265        let ptr = jit_v2_struct_alloc(16);
266        assert!(!ptr.is_null());
267
268        jit_v2_struct_set_i64(ptr, 8, -42);
269        assert_eq!(jit_v2_struct_get_i64(ptr, 8), -42);
270
271        jit_v2_struct_set_i64(ptr, 8, i64::MAX);
272        assert_eq!(jit_v2_struct_get_i64(ptr, 8), i64::MAX);
273
274        jit_v2_struct_release(ptr, 16);
275    }
276
277    #[test]
278    fn test_i32_field_access() {
279        // HeapHeader (8) + one i32 field (4) + padding (4) = 16 bytes
280        let ptr = jit_v2_struct_alloc(16);
281        assert!(!ptr.is_null());
282
283        jit_v2_struct_set_i32(ptr, 8, 999);
284        assert_eq!(jit_v2_struct_get_i32(ptr, 8), 999);
285
286        jit_v2_struct_set_i32(ptr, 8, -1);
287        assert_eq!(jit_v2_struct_get_i32(ptr, 8), -1);
288
289        jit_v2_struct_release(ptr, 16);
290    }
291
292    #[test]
293    fn test_bool_field_access() {
294        // HeapHeader (8) + one bool (1) — allocate 16 for alignment
295        let ptr = jit_v2_struct_alloc(16);
296        assert!(!ptr.is_null());
297
298        // Initially zeroed (false)
299        assert_eq!(jit_v2_struct_get_bool(ptr, 8), 0);
300
301        jit_v2_struct_set_bool(ptr, 8, 1);
302        assert_eq!(jit_v2_struct_get_bool(ptr, 8), 1);
303
304        jit_v2_struct_set_bool(ptr, 8, 0);
305        assert_eq!(jit_v2_struct_get_bool(ptr, 8), 0);
306
307        jit_v2_struct_release(ptr, 16);
308    }
309
310    #[test]
311    fn test_ptr_field_access() {
312        // HeapHeader (8) + one pointer (8) = 16 bytes
313        let ptr = jit_v2_struct_alloc(16);
314        assert!(!ptr.is_null());
315
316        // Initially null (zeroed)
317        assert!(jit_v2_struct_get_ptr(ptr, 8).is_null());
318
319        // Allocate a nested struct and store its pointer
320        let inner = jit_v2_struct_alloc(16);
321        jit_v2_struct_set_ptr(ptr, 8, inner);
322        assert_eq!(jit_v2_struct_get_ptr(ptr, 8), inner);
323
324        jit_v2_struct_release(inner, 16);
325        jit_v2_struct_release(ptr, 16);
326    }
327
328    #[test]
329    fn test_mixed_fields() {
330        // type Record { x: number, count: int, flag: bool }
331        // Layout: HeapHeader(8) + x:f64(8) + count:i64(8) + flag:bool(1) + pad(7) = 32
332        let ptr = jit_v2_struct_alloc(32);
333        assert!(!ptr.is_null());
334
335        jit_v2_struct_set_f64(ptr, 8, 1.5);
336        jit_v2_struct_set_i64(ptr, 16, 100);
337        jit_v2_struct_set_bool(ptr, 24, 1);
338
339        assert_eq!(jit_v2_struct_get_f64(ptr, 8), 1.5);
340        assert_eq!(jit_v2_struct_get_i64(ptr, 16), 100);
341        assert_eq!(jit_v2_struct_get_bool(ptr, 24), 1);
342
343        jit_v2_struct_release(ptr, 32);
344    }
345
346    #[test]
347    fn test_retain_release_refcount() {
348        let ptr = jit_v2_struct_alloc(16);
349        assert!(!ptr.is_null());
350
351        // Initial refcount = 1
352        assert_eq!(jit_v2_struct_refcount(ptr), 1);
353
354        // Retain bumps to 2
355        jit_v2_struct_retain(ptr);
356        assert_eq!(jit_v2_struct_refcount(ptr), 2);
357
358        // Another retain bumps to 3
359        jit_v2_struct_retain(ptr);
360        assert_eq!(jit_v2_struct_refcount(ptr), 3);
361
362        // Release decrements to 2
363        jit_v2_struct_release(ptr, 16);
364        assert_eq!(jit_v2_struct_refcount(ptr), 2);
365
366        // Release decrements to 1
367        jit_v2_struct_release(ptr, 16);
368        assert_eq!(jit_v2_struct_refcount(ptr), 1);
369
370        // Final release deallocates (refcount reaches 0)
371        jit_v2_struct_release(ptr, 16);
372        // ptr is now dangling — do not read
373    }
374
375    #[test]
376    fn test_null_safety() {
377        // All operations should be safe with null pointers
378        jit_v2_struct_retain(std::ptr::null_mut());
379        jit_v2_struct_release(std::ptr::null_mut(), 16);
380        assert_eq!(jit_v2_struct_refcount(std::ptr::null()), 0);
381    }
382
383    #[test]
384    fn test_alloc_too_small() {
385        // Size < 8 should return null (can't fit the header)
386        let ptr = jit_v2_struct_alloc(4);
387        assert!(ptr.is_null());
388
389        let ptr = jit_v2_struct_alloc(0);
390        assert!(ptr.is_null());
391    }
392
393    #[test]
394    fn test_zeroed_fields() {
395        // All field bytes should be zero after allocation
396        let ptr = jit_v2_struct_alloc(32);
397        assert!(!ptr.is_null());
398
399        // Fields at offsets 8, 16, 24 should all read as zero
400        assert_eq!(jit_v2_struct_get_f64(ptr, 8), 0.0);
401        assert_eq!(jit_v2_struct_get_f64(ptr, 16), 0.0);
402        assert_eq!(jit_v2_struct_get_i64(ptr, 8), 0);
403        assert_eq!(jit_v2_struct_get_i32(ptr, 8), 0);
404        assert_eq!(jit_v2_struct_get_bool(ptr, 8), 0);
405
406        jit_v2_struct_release(ptr, 32);
407    }
408
409    #[test]
410    fn test_overwrite_field() {
411        let ptr = jit_v2_struct_alloc(16);
412        assert!(!ptr.is_null());
413
414        jit_v2_struct_set_f64(ptr, 8, 1.0);
415        assert_eq!(jit_v2_struct_get_f64(ptr, 8), 1.0);
416
417        // Overwrite with new value
418        jit_v2_struct_set_f64(ptr, 8, 2.0);
419        assert_eq!(jit_v2_struct_get_f64(ptr, 8), 2.0);
420
421        jit_v2_struct_release(ptr, 16);
422    }
423}