Skip to main content

dodo_bindgen_codegen/
fn_.rs

1use crate::util::*;
2use crate::{FfiFunction, FfiType, RustType, TsModule};
3use dodo_bindgen_rust_ir::{ItemFn, Receiver};
4use heck::ToLowerCamelCase as _;
5use proc_macro2::TokenStream;
6use syn::Ident;
7
8use super::ty::TypeExt as _;
9
10#[derive(Clone, Debug, Default)]
11pub struct FunctionDefs {
12    pub functions: Vec<TsMethod>,
13}
14
15#[derive(Clone, Debug)]
16pub struct TsMethod {
17    pub non_blocking: bool,
18    pub ident: Ident,
19    pub inputs: Vec<RustType>,
20    pub output: RustType,
21    pub assoc: Option<Receiver>,
22    pub ffi_ident: Ident,
23    pub self_ty: Option<Ident>,
24}
25
26/* -------------------------------------------------------------------------- */
27
28// MARK: transform
29
30pub trait ItemFnExt {
31    fn unwrap(self, module: &mut TsModule, class: Option<(Ident, Receiver)>) -> TsMethod;
32}
33
34impl ItemFnExt for ItemFn {
35    fn unwrap(self, module: &mut TsModule, class: Option<(Ident, Receiver)>) -> TsMethod {
36        let mut ffi_inputs = Vec::new();
37        let mut inputs = Vec::new();
38        for arg in self.inputs {
39            ffi_inputs.push(FfiType::from(arg.abi));
40            inputs.push(arg.ty.unwrap(module));
41        }
42
43        let output = self.output.unwrap(module);
44        let ffi_ident = format_ident!("{}", self.symbol);
45
46        module.ffi_lib.interface.push_fn(FfiFunction {
47            ident: ffi_ident.clone(),
48            inputs: ffi_inputs,
49            output: FfiType::from(self.output_abi),
50            non_blocking: self.non_blocking,
51        });
52
53        let (self_ty, assoc) = class.unzip();
54        let method = TsMethod {
55            non_blocking: self.non_blocking,
56            ident: format_ident!("{}", self.name.to_lower_camel_case()),
57            inputs,
58            output,
59            assoc,
60            ffi_ident,
61            self_ty,
62        };
63        module.uses_pin |= method.needs_pin();
64        method
65    }
66}
67
68impl FunctionDefs {
69    pub fn push(&mut self, method: TsMethod) {
70        self.functions.push(method);
71    }
72}
73
74/* -------------------------------------------------------------------------- */
75
76// MARK: print
77
78fn return_stmt(output: &RustType) -> TokenStream {
79    match output {
80        RustType::Void => TokenStream::new(),
81        RustType::Numeric(_)
82        | RustType::Boolean
83        | RustType::FnPtr(_)
84        | RustType::Ptr(_)
85        | RustType::PtrMut(_)
86        | RustType::Ref(_)
87        | RustType::RefMut(_)
88        | RustType::Unsupported => quote! {
89            return out! as #output;
90        },
91        RustType::Custom { from_rust, .. } => {
92            let value = raw(from_rust.replace("{}", "out"));
93            quote! {
94                return #value;
95            }
96        },
97        RustType::UserDefined(ident) => quote! {
98            return Reflect.construct(RustPrototype, [out!], #ident) as #ident;
99        },
100        rest => quote! {
101            return new #rest(out!) as #output;
102        },
103    }
104}
105
106impl TsMethod {
107    pub fn needs_pin(&self) -> bool {
108        self.non_blocking
109            && self
110                .inputs
111                .iter()
112                .any(|input| matches!(input, RustType::Custom { .. }))
113    }
114
115    fn is_constructor(&self) -> bool {
116        self.ident == "new"
117            && self.assoc == Some(Receiver::Static)
118            && !self.non_blocking
119            && self
120                .self_ty
121                .as_ref()
122                .is_some_and(|class| self.output == RustType::UserDefined(class.clone()))
123    }
124
125    pub fn print(&self) -> TokenStream {
126        let TsMethod {
127            ident,
128            inputs,
129            output,
130            assoc,
131            ffi_ident,
132            self_ty,
133            non_blocking,
134        } = self;
135
136        let inputs: Vec<&RustType> = inputs.iter().filter(|ty| **ty != RustType::Void).collect();
137        let explicit = match assoc {
138            Some(Receiver::Ref | Receiver::RefMut | Receiver::Owned) => &inputs[1..],
139            Some(Receiver::Static) | None => &inputs[..],
140        };
141
142        let borrow = |sync: TokenStream, async_: TokenStream| {
143            if *non_blocking {
144                quote! { using borrow = await #async_(this); }
145            } else {
146                quote! { using borrow = #sync(this); }
147            }
148        };
149        let mut static_ = TokenStream::new();
150        let mut pre = Vec::new();
151        let mut call_args = Vec::new();
152        match assoc {
153            Some(Receiver::Static) => static_ = quote! { static },
154            Some(Receiver::Ref) => {
155                pre.push(borrow(
156                    quote! { __dodo_borrow },
157                    quote! { __dodo_borrow_async },
158                ));
159                call_args.push(quote! { borrow.ptr });
160            },
161            Some(Receiver::RefMut) => {
162                pre.push(borrow(
163                    quote! { __dodo_borrow_mut },
164                    quote! { __dodo_borrow_mut_async },
165                ));
166                call_args.push(quote! { borrow.ptr });
167            },
168            Some(Receiver::Owned) => {
169                pre.push(if *non_blocking {
170                    quote! { const ptr = await __dodo_take_async(this); }
171                } else {
172                    quote! { const ptr = this.take(); }
173                });
174                call_args.push(quote! { ptr });
175            },
176            None => (),
177        }
178
179        let mut fn_args = Vec::new();
180        let mut pinned = Vec::new();
181        for (i, input) in explicit.iter().enumerate() {
182            let arg = format_ident!("arg_{i}");
183            fn_args.push(quote! { #arg: #input });
184
185            let value = match input {
186                RustType::Numeric(_)
187                | RustType::Boolean
188                | RustType::FnPtr(_)
189                | RustType::Ptr(_)
190                | RustType::PtrMut(_)
191                | RustType::Ref(_)
192                | RustType::RefMut(_)
193                | RustType::Unsupported => quote! { #arg },
194                RustType::Custom { to_rust, .. } => {
195                    raw(to_rust.replace("{}", &arg.to_string())).to_token_stream()
196                },
197                _ => quote! { #arg.take() },
198            };
199
200            if *non_blocking && matches!(input, RustType::Custom { .. }) {
201                let temp = format_ident!("pin_{i}");
202                pre.push(quote! { const #temp = #value; });
203                pinned.push(quote! { #temp });
204                pinned.push(quote! { #arg });
205                call_args.push(quote! { #temp });
206            } else {
207                call_args.push(value);
208            }
209        }
210
211        let (async_, await_) = if *non_blocking {
212            (quote! { async }, quote! { await })
213        } else {
214            (TokenStream::new(), TokenStream::new())
215        };
216        let fn_output = match (output, non_blocking) {
217            (RustType::Void, false) => TokenStream::new(),
218            (rest, true) => quote! { : Promise<#rest> },
219            (rest, false) => quote! { : #rest },
220        };
221
222        let call = quote! { #await_ symbols.#ffi_ident(#(#call_args),*) };
223        if self.is_constructor() {
224            return quote! {
225                constructor(#(#fn_args),*) {
226                    super(#call);
227                }
228            };
229        }
230        let call = match output {
231            RustType::Void => quote! { #call; },
232            _ => quote! { const out = #call; },
233        };
234        let ret = return_stmt(output);
235
236        let body = if pinned.is_empty() {
237            quote! {
238                #(#pre)*
239                #call
240                #ret
241            }
242        } else {
243            quote! {
244                #(#pre)*
245                const pinned = [#(#pinned),*];
246                __dodo_pinned.add(pinned);
247                try {
248                    #call
249                    #ret
250                } finally {
251                    __dodo_pinned.delete(pinned);
252                }
253            }
254        };
255
256        let head = if self_ty.is_some() {
257            quote! { #static_ #async_ #ident }
258        } else {
259            quote! { export #async_ function #ident }
260        };
261
262        quote! {
263            #head(#(#fn_args),*) #fn_output {
264                #body
265            }
266        }
267    }
268}
269
270impl ToTokens for FunctionDefs {
271    fn to_tokens(&self, tokens: &mut TokenStream) {
272        for method in &self.functions {
273            tokens.extend(method.print());
274        }
275    }
276}
277
278/* -------------------------------------------------------------------------- */
279
280// MARK: print tests
281
282#[cfg(test)]
283mod print_tests {
284    use super::*;
285    use crate::TsModule;
286    use dodo_bindgen_rust_ir::{Abi, Arg, Type, TypeNumeric};
287
288    fn arg(ty: Type, abi: Abi) -> Arg {
289        Arg {
290            name: "arg".to_string(),
291            ty,
292            abi,
293        }
294    }
295
296    fn func(inputs: Vec<Arg>, output: Type, output_abi: Abi) -> ItemFn {
297        ItemFn {
298            name: "test_fn".to_string(),
299            symbol: "__dodo_crate_test_fn".to_string(),
300            doc: String::new(),
301            non_blocking: false,
302            inputs,
303            output,
304            output_abi,
305        }
306    }
307
308    fn print(item: ItemFn) -> String {
309        let mut module = TsModule::default();
310        module.ffi_lib.lib_name = "lib".to_string();
311        let method = item.unwrap(&mut module, None);
312        module.functions.push(method);
313        deno_fmt(format!(
314            "{}\n{}",
315            module.ffi_lib.to_token_stream(),
316            module.functions.to_token_stream()
317        ))
318    }
319
320    #[test]
321    fn test_empty() {
322        insta::assert_snapshot!(print(func(vec![], Type::Void, Abi::Void)));
323    }
324
325    #[test]
326    fn test_return() {
327        let u8_ty = Type::Numeric(TypeNumeric::U8);
328        insta::assert_snapshot!(print(func(
329            vec![arg(u8_ty.clone(), Abi::U8)],
330            u8_ty,
331            Abi::U8
332        )));
333    }
334
335    fn custom(ts_type: &str, to_rust: &str, from_rust: &str) -> Type {
336        Type::Custom {
337            ts_type: ts_type.to_string(),
338            to_rust: to_rust.to_string(),
339            from_rust: from_rust.to_string(),
340            helper: String::new(),
341        }
342    }
343
344    #[test]
345    fn test_many_args() {
346        let custom = Type::Object {
347            module_path: "crate".to_string(),
348            name: "CustomType".to_string(),
349        };
350        insta::assert_snapshot!(print(func(
351            vec![
352                arg(
353                    Type::RefMut(Box::new(Type::Slice(Box::new(Type::Numeric(
354                        TypeNumeric::U8,
355                    ))))),
356                    Abi::Pointer,
357                ),
358                arg(Type::Ref(Box::new(Type::Str)), Abi::Pointer),
359                arg(Type::Box(Box::new(custom)), Abi::Pointer),
360                arg(Type::Vec(Box::new(Type::Bool)), Abi::Pointer),
361            ],
362            Type::Void,
363            Abi::Void,
364        )));
365    }
366
367    #[test]
368    fn test_custom() {
369        insta::assert_snapshot!(print(func(
370            vec![arg(
371                custom("string", "`${{}}`.codePointAt(0)!", "''"),
372                Abi::U32,
373            )],
374            custom(
375                "Float64Array<ArrayBuffer>",
376                "",
377                "__dodo_decode_vec({}, Float64Array)",
378            ),
379            Abi::Pointer,
380        )));
381    }
382
383    #[test]
384    fn test_non_blocking_pins_buffers() {
385        let mut item = func(
386            vec![
387                arg(
388                    custom("Uint8Array<ArrayBuffer>", "__dodo_view({})", ""),
389                    Abi::Buffer,
390                ),
391                arg(custom("string", "__dodo_encode_str({})", ""), Abi::Buffer),
392            ],
393            Type::Void,
394            Abi::Void,
395        );
396        item.non_blocking = true;
397        let mut module = TsModule::default();
398        let method = item.unwrap(&mut module, None);
399        assert!(module.uses_pin);
400        module.functions.push(method);
401        insta::assert_snapshot!(deno_fmt(module.functions.to_token_stream().to_string()));
402    }
403}