Skip to content

Commit 9b20147

Browse files
committed
fix: implement deallocation for fixed-length lists with heap elements
Fixed two bugs in the core ABI for FixedLengthList types: 1. `deallocate` was `todo!()`, causing a panic when flat deallocation was needed (e.g. `list<own<resource>, 3>`). Now uses `flat_for_each_record_type` to iterate element types. 2. `deallocate_indirect` was a silent no-op `=> {}`, leaking heap-allocated elements (e.g. strings in `list<string, 3>`). Now iterates each element's memory offset and deallocates it using `deallocate_indirect_fields`. Added both a codegen test (verifying `cabi_post_` functions are generated) and a runtime test with allocation tracking (verifying no memory leaks when passing/returning fixed-length lists of strings).
1 parent fd57889 commit 9b20147

File tree

7 files changed

+133
-7
lines changed

7 files changed

+133
-7
lines changed

crates/core/src/abi.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,7 +2404,13 @@ impl<'a, B: Bindgen> Generator<'a, B> {
24042404
TypeDefKind::Resource => unreachable!(),
24052405
TypeDefKind::Unknown => unreachable!(),
24062406

2407-
TypeDefKind::FixedLengthList(..) => todo!(),
2407+
TypeDefKind::FixedLengthList(element, size) => {
2408+
self.flat_for_each_record_type(
2409+
ty,
2410+
iter::repeat_n(element, *size as usize),
2411+
|me, ty| me.deallocate(ty, what),
2412+
);
2413+
}
24082414
TypeDefKind::Map(..) => todo!(),
24092415
},
24102416
}
@@ -2526,7 +2532,10 @@ impl<'a, B: Bindgen> Generator<'a, B> {
25262532
TypeDefKind::Future(_) => unreachable!(),
25272533
TypeDefKind::Stream(_) => unreachable!(),
25282534
TypeDefKind::Unknown => unreachable!(),
2529-
TypeDefKind::FixedLengthList(_, _) => {}
2535+
TypeDefKind::FixedLengthList(element, size) => {
2536+
let tys: Vec<Type> = iter::repeat_n(*element, *size as usize).collect();
2537+
self.deallocate_indirect_fields(&tys, addr, offset, what);
2538+
}
25302539
TypeDefKind::Map(..) => todo!(),
25312540
},
25322541
}

crates/rust/tests/codegen.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,44 @@ mod newtyped_list {
142142
}
143143
}
144144
}
145+
146+
#[test]
147+
fn fixed_length_list_deallocation() {
148+
let wit = r#"
149+
package test:fll-dealloc;
150+
151+
world test {
152+
export return-string-array: func() -> list<string, 3>;
153+
export accept-string-array: func(a: list<string, 3>);
154+
}
155+
"#;
156+
157+
let mut resolve = wit_bindgen_core::wit_parser::Resolve::default();
158+
let pkg = resolve.push_str("test.wit", wit).unwrap();
159+
let world = resolve.select_world(&[pkg], None).unwrap();
160+
161+
let opts = wit_bindgen_rust::Opts {
162+
generate_all: true,
163+
..Default::default()
164+
};
165+
166+
let mut generator = opts.build();
167+
let mut files = wit_bindgen_core::Files::default();
168+
use wit_bindgen_core::WorldGenerator;
169+
generator.generate(&mut resolve, world, &mut files).unwrap();
170+
171+
let (_name, contents) = files.iter().next().unwrap();
172+
let code = String::from_utf8_lossy(contents);
173+
174+
// Verify post-return deallocation functions are generated for
175+
// fixed-length lists containing heap-allocated elements (strings)
176+
assert!(
177+
code.contains("cabi_post_"),
178+
"expected cabi_post_ deallocation function for fixed-length list \
179+
with string elements, but none was generated"
180+
);
181+
}
182+
145183
#[allow(unused, reason = "testing codegen, not functionality")]
146184
mod retyped_list {
147185
use std::ops::Deref;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::alloc::{GlobalAlloc, Layout, System};
2+
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
3+
4+
#[global_allocator]
5+
static ALLOC: A = A;
6+
7+
static ALLOC_AMT: AtomicUsize = AtomicUsize::new(0);
8+
9+
struct A;
10+
11+
unsafe impl GlobalAlloc for A {
12+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
13+
let ptr = System.alloc(layout);
14+
if !ptr.is_null() {
15+
ALLOC_AMT.fetch_add(layout.size(), SeqCst);
16+
}
17+
return ptr;
18+
}
19+
20+
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
21+
// Poison all deallocations to try to catch any use-after-free in the
22+
// bindings as early as possible.
23+
std::ptr::write_bytes(ptr, 0xde, layout.size());
24+
ALLOC_AMT.fetch_sub(layout.size(), SeqCst);
25+
System.dealloc(ptr, layout)
26+
}
27+
}
28+
pub fn get() -> usize {
29+
ALLOC_AMT.load(SeqCst)
30+
}

tests/runtime/fixed-length-lists/runner.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,66 @@ include!(env!("BINDINGS"));
44

55
use test::fixed_length_lists::to_test::*;
66

7+
mod alloc;
8+
9+
struct Guard {
10+
me_before: usize,
11+
remote_before: u32,
12+
}
13+
14+
impl Guard {
15+
fn new() -> Guard {
16+
Guard {
17+
me_before: alloc::get(),
18+
remote_before: allocated_bytes(),
19+
}
20+
}
21+
}
22+
23+
impl Drop for Guard {
24+
fn drop(&mut self) {
25+
assert_eq!(self.me_before, alloc::get());
26+
assert_eq!(self.remote_before, allocated_bytes());
27+
}
28+
}
29+
730
struct Component;
831

932
export!(Component);
1033

1134
impl Guest for Component {
1235
fn run() {
13-
list_param([1, 2, 3, 4]);
14-
list_param2([[1, 2], [3, 4]]);
15-
list_param3([
16-
-1, 2, -3, 4, -5, 6, -7, 8, -9, 10, -11, 12, -13, 14, -15, 16, -17, 18, -19, 20,
17-
]);
1836
{
37+
let _guard = Guard::new();
38+
list_param([1, 2, 3, 4]);
39+
}
40+
{
41+
let _guard = Guard::new();
42+
list_param2([[1, 2], [3, 4]]);
43+
}
44+
{
45+
let _guard = Guard::new();
46+
list_param3([
47+
-1, 2, -3, 4, -5, 6, -7, 8, -9, 10, -11, 12, -13, 14, -15, 16, -17, 18, -19, 20,
48+
]);
49+
}
50+
{
51+
let _guard = Guard::new();
1952
let result = list_result();
2053
assert_eq!(result, [b'0', b'1', b'A', b'B', b'a', b'b', 128, 255]);
2154
}
2255
{
56+
let _guard = Guard::new();
2357
let result = list_minmax16([0, 1024, 32768, 65535], [1, 2048, -32767, -2]);
2458
assert_eq!(result, ([0, 1024, 32768, 65535], [1, 2048, -32767, -2]));
2559
}
2660
{
61+
let _guard = Guard::new();
2762
let result = list_minmax_float([2.0, -42.0], [0.25, -0.125]);
2863
assert_eq!(result, ([2.0, -42.0], [0.25, -0.125]));
2964
}
3065
{
66+
let _guard = Guard::new();
3167
let result =
3268
list_roundtrip([b'a', b'b', b'c', b'd', 0, 1, 2, 3, b'A', b'B', b'Y', b'Z']);
3369
assert_eq!(
@@ -36,13 +72,15 @@ impl Guest for Component {
3672
);
3773
}
3874
{
75+
let _guard = Guard::new();
3976
let result = nested_roundtrip([[1, 5], [42, 1_000_000]], [[-1, 3], [-2_000_000, 4711]]);
4077
assert_eq!(
4178
result,
4279
([[1, 5], [42, 1_000_000]], [[-1, 3], [-2_000_000, 4711]])
4380
);
4481
}
4582
{
83+
let _guard = Guard::new();
4684
let result = large_roundtrip(
4785
[[1, 5], [42, 1_000_000]],
4886
[
@@ -66,6 +104,7 @@ impl Guest for Component {
66104
);
67105
}
68106
{
107+
let _guard = Guard::new();
69108
let result = nightmare_on_cpp([Nested { l: [1, -1] }, Nested { l: [2, -2] }]);
70109
assert_eq!(result[0].l, [1, -1]);
71110
assert_eq!(result[1].l, [2, -2]);

tests/runtime/fixed-length-lists/test.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ std::array<to_test::Nested, 2>
4949
to_test::NightmareOnCpp(std::array<to_test::Nested, 2> a) {
5050
return a;
5151
}
52+
uint32_t to_test::AllocatedBytes() {
53+
return 0;
54+
}

tests/runtime/fixed-length-lists/test.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ struct Component;
44

55
export!(Component);
66

7+
mod alloc;
8+
79
use crate::exports::test::fixed_length_lists::to_test::Nested;
810

911
impl exports::test::fixed_length_lists::to_test::Guest for Component {
12+
fn allocated_bytes() -> u32 {
13+
alloc::get().try_into().unwrap()
14+
}
1015
fn list_param(a: [u32; 4]) {
1116
assert_eq!(a, [1, 2, 3, 4]);
1217
}

tests/runtime/fixed-length-lists/test.wit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface to-test {
2020
}
2121

2222
nightmare-on-cpp: func(a: list<nested, 2>) -> list<nested, 2>;
23+
24+
allocated-bytes: func() -> u32;
2325
}
2426

2527
world test {

0 commit comments

Comments
 (0)