|
| 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