Skip to content

Commit 2cc6bd9

Browse files
committed
added closure example
1 parent 0db719e commit 2cc6bd9

1 file changed

Lines changed: 378 additions & 0 deletions

File tree

examples/closures/main.rs

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
//! This example shows how one can implement dynamic closures with captures.
2+
//!
3+
//! Let's imagine a scenario such as
4+
//! ```
5+
//! let f0 = |x| a + x + 1;
6+
//! let f1 = |x| a + x + b;
7+
//! let fs = [f0, f1];
8+
//! ```
9+
//! These two closures capture different values. However; They're put in the same array.
10+
//! This is meant to be valid as the closures have the same type signature `int -> int`.
11+
//!
12+
//! We need a way to type-erase the captures and make all closures of the same type have the same size.
13+
//!
14+
//! A simple way to accomplish this is to make all closure a pair of a function pointer, and and
15+
//! opaque capture pointer.
16+
//!
17+
//! This way; the captures can be dynamically dereferenced from the pointer and all closures will
18+
//! be the exact same size.
19+
//!
20+
//! ```
21+
//! let f0 = { data: &(a) , func: |data, x| (*data).a + x + 1 };
22+
//! let f1 = { data: &(a, b), func: |data, x| (*data).a + x + (*data).b };
23+
//! let fs = [f0, f1];
24+
//! ```
25+
//!
26+
//! To link against system libraries and produce a binary on Linux or MacOS, you can use `gcc` or `clang`
27+
//!
28+
//! `$ cargo run --example closures -- -o closures.o`
29+
//! `$ clang closures.o -o closures`
30+
//! `$ ./closures; echo $?`
31+
32+
use cranelift::prelude::isa::CallConv;
33+
use cranelift::prelude::{self as cl, InstBuilder, Type};
34+
use cranelift::prelude::{FunctionBuilder, MemFlags};
35+
use cranelift_examples::{
36+
declare_main, function_builder_from_declaration, signature_from_decl, skip_boilerplate,
37+
};
38+
use cranelift_module::{FuncId, Linkage, Module};
39+
use cranelift_object::ObjectModule;
40+
41+
fn main() {
42+
skip_boilerplate(b"closures", |ctx, fctx, module, _args| {
43+
let main_func_id = declare_main(module);
44+
let f0_funcid = declare_f0_real_function(module);
45+
let f1_funcid = declare_f1_real_function(module);
46+
47+
// fn main() {
48+
// let a = 1;
49+
// let b = 2;
50+
// let x = 3;
51+
//
52+
// let f0 = |x| a + x + 1;
53+
// let f1 = |x| a + x + b;
54+
//
55+
// let t = f0(x);
56+
// let u = f1(x);
57+
//
58+
// return t + u;
59+
// }
60+
{
61+
let (mut fbuilder, _) =
62+
function_builder_from_declaration(module, &mut ctx.func, fctx, main_func_id);
63+
64+
// let a = 1;
65+
// let b = 2;
66+
// let x = 3;
67+
let [a, b, x] = [1, 2, 3].map(|n| fbuilder.ins().iconst(cl::types::I32, n));
68+
69+
// let f0 = |x| a + x + 1;
70+
// let f1 = |x| a + x + b;
71+
//
72+
// // -- Although the way we represent it in Cranelift looks like -- //
73+
//
74+
// let f0 = { data: &(a) , func: |data, x| (*data).a + x + 1 };
75+
// let f1 = { data: &(a, b), func: |data, x| (*data).a + x + (*data).b };
76+
let f0 = construct_closure(module, &mut fbuilder, f0_funcid, &[a]);
77+
let f1 = construct_closure(module, &mut fbuilder, f1_funcid, &[a, b]);
78+
79+
// let t = f0(x);
80+
// let u = f1(x);
81+
//
82+
// // -- Although the way we represent it in Cranelift looks like -- //
83+
//
84+
// let t = (f0.func)(f0.data, x);
85+
// let u = (f1.func)(f1.data, x)
86+
let t = f0.call(&mut fbuilder, &[x])[0];
87+
let u = f1.call(&mut fbuilder, &[x])[0];
88+
89+
// return t + u;
90+
let sum = fbuilder.ins().iadd(t, u);
91+
fbuilder.ins().return_(&[sum]);
92+
93+
fbuilder.finalize();
94+
95+
println!("fn main:\n{}", &ctx.func);
96+
97+
module.define_function(main_func_id, ctx).unwrap();
98+
}
99+
100+
// fn f0(a: int, x: int) -> int {
101+
// return a + x + 1;
102+
// }
103+
{
104+
let (mut fbuilder, block) =
105+
function_builder_from_declaration(module, &mut ctx.func, fctx, f0_funcid);
106+
107+
let a = fbuilder.block_params(block)[0];
108+
let x = fbuilder.block_params(block)[1];
109+
110+
let n = fbuilder.ins().iadd(a, x);
111+
let n = fbuilder.ins().iadd_imm(n, 1);
112+
113+
fbuilder.ins().return_(&[n]);
114+
115+
fbuilder.finalize();
116+
117+
println!("fn f0:\n{}", &ctx.func);
118+
119+
module.define_function(f0_funcid, ctx).unwrap();
120+
}
121+
122+
// fn f1(a: int, b: int, x: int) -> int {
123+
// return a + x + b;
124+
// }
125+
{
126+
let (mut fbuilder, block) =
127+
function_builder_from_declaration(module, &mut ctx.func, fctx, f1_funcid);
128+
129+
let a = fbuilder.block_params(block)[0];
130+
let b = fbuilder.block_params(block)[1];
131+
let x = fbuilder.block_params(block)[2];
132+
133+
let n = fbuilder.ins().iadd(a, x);
134+
let n = fbuilder.ins().iadd(n, b);
135+
136+
fbuilder.ins().return_(&[n]);
137+
138+
fbuilder.finalize();
139+
140+
println!("fn f1:\n{}", &ctx.func);
141+
142+
module.define_function(f1_funcid, ctx).unwrap();
143+
}
144+
});
145+
}
146+
147+
// Declare the underlying function for the closure `f0`.
148+
//
149+
// All the captures are implicitly added as parameter.
150+
//
151+
// fn f0(a: int, x: int) -> int { a + x + 1 }
152+
fn declare_f0_real_function(module: &mut ObjectModule) -> FuncId {
153+
// (a: int, x: int) -> int
154+
let sig = cl::Signature {
155+
call_conv: CallConv::Fast,
156+
params: vec![cl::AbiParam::new(cl::types::I32); 2],
157+
returns: vec![cl::AbiParam::new(cl::types::I32)],
158+
};
159+
160+
module
161+
.declare_function("f0_real_function", Linkage::Local, &sig)
162+
.unwrap()
163+
}
164+
165+
// Declare the underlying function for the closure `f0`.
166+
//
167+
// All the captures are implicitly added as parameter.
168+
//
169+
// fn f1(a: int, b: int, x: int) -> int { a + x + 1 }
170+
fn declare_f1_real_function(module: &mut ObjectModule) -> FuncId {
171+
// (a: int, b: int, x: int) -> int
172+
let sig = cl::Signature {
173+
call_conv: CallConv::Fast,
174+
params: vec![cl::AbiParam::new(cl::types::I32); 3],
175+
returns: vec![cl::AbiParam::new(cl::types::I32)],
176+
};
177+
178+
module
179+
.declare_function("f1_real_function", Linkage::Local, &sig)
180+
.unwrap()
181+
}
182+
183+
struct Closure {
184+
data: cl::Value,
185+
func: cl::Value,
186+
sig: cl::Signature,
187+
}
188+
189+
impl Closure {
190+
fn call<'a>(
191+
&self,
192+
fbuilder: &'a mut FunctionBuilder<'_>,
193+
params: &[cl::Value],
194+
) -> &'a [cl::Value] {
195+
let mut real_params = vec![self.data];
196+
real_params.extend_from_slice(params);
197+
let sigref = fbuilder.import_signature(self.sig.clone());
198+
let call = fbuilder
199+
.ins()
200+
.call_indirect(sigref, self.func, &real_params);
201+
fbuilder.inst_results(call)
202+
}
203+
}
204+
205+
// When invoking the closure, we can't know the types of the captures.
206+
// However; here where we construct the closure we do know the types.
207+
//
208+
// To make this work we need to perform some form of type erasure, to make all closures with
209+
// the same signatures behave the same regardless of captures.
210+
//
211+
// We do that by first boxing all the captures, and then create an intermediate function which
212+
// dereferences the captures and forwards them to the 'real' function pointer.
213+
fn construct_closure(
214+
module: &mut ObjectModule,
215+
fbuilder: &mut FunctionBuilder<'_>,
216+
closure_fn: FuncId,
217+
captures: &[cl::Value],
218+
) -> Closure {
219+
let boxed_captures = stack_alloc_captures(module, fbuilder, captures);
220+
221+
let (forwarding_func_ref, sig) = {
222+
let capture_types = captures
223+
.iter()
224+
.map(|&v| fbuilder.func.stencil.dfg.value_type(v))
225+
.collect::<Vec<_>>();
226+
227+
let (func_id, sig) = create_forwarding_func(module, closure_fn, &capture_types);
228+
229+
let fref = module.declare_func_in_func(func_id, &mut fbuilder.func);
230+
let size_t = module.isa().pointer_type();
231+
(fbuilder.ins().func_addr(size_t, fref), sig)
232+
};
233+
234+
Closure {
235+
data: boxed_captures,
236+
func: forwarding_func_ref,
237+
sig,
238+
}
239+
}
240+
241+
// If we have a closure with the user-facing signature `(int, int) -> int`
242+
//
243+
// Then the closure's actual signature will be `(*void, int, int) -> int`
244+
// Where `*void` represents a pointer to the captures.
245+
//
246+
// We need to dereferences those captures and forward them to the real function defined where the
247+
// closure is created (in this example `f0_real_function` and `f1_real_function`).
248+
//
249+
// We do so with what we here call the "forwarding function".
250+
//
251+
// So for the `f1` we'd define.
252+
//
253+
// ```
254+
// fn closure_forward_f1_real_function(captures: *void, x: int) -> int {
255+
// let a = *(captures + 0);
256+
// let b = *(captures + 4);
257+
// return f1_real_function(a, b, x);
258+
// }
259+
// ```
260+
//
261+
// And then the actual values we will pass around in memory would be.
262+
// ```
263+
// let closure = { data: alloc([1, 2]), func: closure_forward_f1_real_function };
264+
// ```
265+
//
266+
// So that it may be called as
267+
//
268+
// ```
269+
// closure.func(closure.data, 3)
270+
// ```
271+
fn create_forwarding_func(
272+
module: &mut ObjectModule,
273+
f: FuncId,
274+
captys: &[Type],
275+
) -> (FuncId, cl::Signature) {
276+
// In a real compiler, this symbol needs to be generated in a way that's garenteed to be
277+
// unique. You could for example use source code spans, capture type information, or a global counter.
278+
let symbol = format!("closure_forward_{f}");
279+
280+
// Define the signature of the forwarding function to be that of the closure signature but
281+
// with the opaque captures pointer added as the first parameter.
282+
let sig = {
283+
let mut sig = cl::Signature::new(CallConv::Fast);
284+
285+
// The implicit parameters from the capture will be replaced by an opaque pointer instead.
286+
let voidptr = cl::AbiParam::new(module.isa().pointer_type());
287+
sig.params.insert(0, voidptr);
288+
289+
let real_func_sig = signature_from_decl(module, f);
290+
for &p in real_func_sig.params.iter().skip(captys.len()) {
291+
sig.params.push(p);
292+
}
293+
sig.returns = real_func_sig.returns.clone();
294+
295+
sig
296+
};
297+
298+
// Declare the closure forwarding function
299+
let func_id = module
300+
.declare_function(&symbol, Linkage::Local, &sig)
301+
.unwrap();
302+
303+
// Define the contents of the closure forwarding function
304+
{
305+
let mut ctx = cl::codegen::Context::new();
306+
let mut fctx = cl::FunctionBuilderContext::new();
307+
308+
let mut closure = cl::FunctionBuilder::new(&mut ctx.func, &mut fctx);
309+
closure.func.signature = sig.clone();
310+
311+
let block = closure.create_block();
312+
closure.append_block_params_for_function_params(block);
313+
closure.switch_to_block(block);
314+
315+
let mut real_call_params =
316+
Vec::with_capacity(captys.len() + closure.func.signature.params.len() - 1);
317+
318+
// Dereference the captures and add them as implicit parameters
319+
let mut offset = 0;
320+
for &ty in captys {
321+
let ptr = closure.block_params(block)[0];
322+
let v = closure.ins().load(ty, MemFlags::new(), ptr, offset);
323+
real_call_params.push(v);
324+
offset += ty.bytes() as i32;
325+
}
326+
327+
// Add all other parameters from the forwarding function
328+
for &v in &closure.block_params(block)[1..] {
329+
real_call_params.push(v);
330+
}
331+
332+
let f_ref = module.declare_func_in_func(f, &mut closure.func);
333+
let call = closure.ins().call(f_ref, &real_call_params);
334+
let returned = closure.inst_results(call).to_vec();
335+
closure.ins().return_(&returned);
336+
337+
module.define_function(func_id, &mut ctx).unwrap();
338+
};
339+
340+
(func_id, sig)
341+
}
342+
343+
fn stack_alloc_captures(
344+
module: &ObjectModule,
345+
fbuilder: &mut FunctionBuilder<'_>,
346+
captures: &[cl::Value],
347+
) -> cl::Value {
348+
let size_t = module.isa().pointer_type();
349+
350+
// Unlike the `struct-layouts` example, we will not be caring about alignment or padding here.
351+
//
352+
// So the size of the stack allocation will just be the sum of the fields we're allocating.
353+
let size = captures
354+
.iter()
355+
.map(|&v| type_of_value(fbuilder, v).bytes())
356+
.sum();
357+
358+
// Create the stack slot for the captures
359+
let slot = fbuilder.create_sized_stack_slot(cl::StackSlotData::new(
360+
cl::StackSlotKind::ExplicitSlot,
361+
size,
362+
0,
363+
));
364+
365+
// Write our captures to the stack allocation
366+
let mut offset = 0;
367+
for &v in captures {
368+
fbuilder.ins().stack_store(v, slot, offset);
369+
offset += type_of_value(fbuilder, v).bytes() as i32;
370+
}
371+
372+
// Return the pointer
373+
fbuilder.ins().stack_addr(size_t, slot, 0)
374+
}
375+
376+
fn type_of_value(fbuilder: &FunctionBuilder<'_>, v: cl::Value) -> Type {
377+
fbuilder.func.stencil.dfg.value_type(v)
378+
}

0 commit comments

Comments
 (0)