ffi_gen/
js.rs

1use crate::import::Instr;
2use crate::{Abi, AbiFunction, AbiObject, AbiType, FunctionType, Interface, NumType, Return, Var};
3use anyhow::Result;
4use genco::prelude::*;
5use genco::tokens::static_literal;
6use heck::*;
7use std::path::Path;
8use std::process::Command;
9
10pub struct JsGenerator {
11    abi: Abi,
12}
13
14impl Default for JsGenerator {
15    fn default() -> Self {
16        Self { abi: Abi::Wasm32 }
17    }
18}
19
20pub struct TsGenerator {
21    docs: bool,
22}
23
24impl Default for TsGenerator {
25    fn default() -> Self {
26        Self { docs: true }
27    }
28}
29
30// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords
31static RESERVED_IDENTIFIERS: [&str; 64] = [
32    "abstract",
33    "arguments",
34    "await",
35    "boolean",
36    "break",
37    "byte",
38    "case",
39    "catch",
40    "char",
41    "class",
42    "const",
43    "continue",
44    "debugger",
45    "default",
46    "delete",
47    "do",
48    "double",
49    "else",
50    "enum",
51    "eval",
52    "export",
53    "extends",
54    "false",
55    "final",
56    "finally",
57    "float",
58    "for",
59    "function",
60    "goto",
61    "if",
62    "implements",
63    "import",
64    "in",
65    "instanceof",
66    "int",
67    "interface",
68    "let",
69    "long",
70    "native",
71    "new",
72    "null",
73    "package",
74    "private",
75    "protected",
76    "public",
77    "return",
78    "short",
79    "static",
80    "super",
81    "switch",
82    "synchronized",
83    "this",
84    "throw",
85    "throws",
86    "transient",
87    "true",
88    "try",
89    "typeof",
90    "var",
91    "void",
92    "volatile",
93    "while",
94    "with",
95    "yield",
96];
97
98fn sanitize_identifier(id: &str) -> String {
99    if RESERVED_IDENTIFIERS.contains(&id) {
100        format!("_{}", id)
101    } else {
102        id.to_string()
103    }
104}
105
106impl TsGenerator {
107    fn gen_doc(
108        &self,
109        items: impl IntoIterator<Item = impl Into<genco::tokens::ItemStr>>,
110    ) -> impl FormatInto<js::JavaScript> {
111        let gen = self.docs;
112        genco::tokens::from_fn(move |t| {
113            let mut it = items.into_iter().peekable();
114
115            if gen && it.peek().is_some() {
116                quote_in! { *t =>
117                    #(static_literal("/**"))
118                    #(for line in it join (#<push>) {
119                        #<space>* #(line.into())
120                    })
121                    #<space>#(static_literal("*/"))
122                }
123            }
124        })
125    }
126    pub fn generate(&self, iface: Interface) -> js::Tokens {
127        quote! {
128            #(static_literal("//")) AUTO GENERATED FILE, DO NOT EDIT.
129            #(static_literal("//"))
130            #(static_literal("//")) Generated by "ffi-gen".
131            #(static_literal("/* tslint:disable */"))
132            #(static_literal("/* eslint:disable */"))
133
134            #(self.gen_doc(&["Main entry point to the library."]))
135            export class Api {
136              #(self.gen_doc(&["API constructor.","","@returns an `Api` instance."]))
137              constructor();
138
139              #(self.gen_doc(&["Initialize the API.", "", "@returns a promise resolved when initialization is done."]))
140              fetch(url, imports): Promise<void>;
141
142              #(for func in iface.functions() join (#<line>#<line>) => #(self.generate_function(func)))
143            }
144
145            #(for obj in iface.objects() => #(self.generate_object(obj)))
146        }
147    }
148
149    fn generate_function(&self, func: AbiFunction) -> js::Tokens {
150        let ffi = Abi::Wasm32.import(&func);
151        let args = self.generate_args(&ffi.abi_args);
152        let ret = self.generate_return_type(ffi.abi_ret.as_ref());
153        let name = self.ident(&func.name);
154        let fun = match &func.ty {
155            FunctionType::Constructor(_) => {
156                quote!(static #name(api: Api, #args): #ret;)
157            }
158            _ => {
159                quote!(#(name)(#args): #ret;)
160            }
161        };
162        quote! {
163            #(self.gen_doc(func.doc))
164            #fun
165        }
166    }
167
168    fn generate_args(&self, abi_args: &[(String, AbiType)]) -> js::Tokens {
169        let len = abi_args.len();
170        let args = quote!(#(for (idx, (name, ty)) in abi_args.iter().enumerate() join (, ) =>
171            #(match ty {
172                AbiType::Option(inner) if idx < len - 1 => #(self.ident(name)): #(self.generate_return_type(Some(inner))) #("| null"),
173                _ => #(self.ident(name)): #(self.generate_return_type(Some(ty)))
174            })
175        ));
176        args
177    }
178
179    fn generate_return_type(&self, ret: Option<&AbiType>) -> js::Tokens {
180        if let Some(ret) = ret {
181            match ret {
182                AbiType::Num(prim) => match prim {
183                    NumType::U8
184                    | NumType::U16
185                    | NumType::U32
186                    | NumType::I8
187                    | NumType::I16
188                    | NumType::I32
189                    | NumType::F32
190                    | NumType::F64 => quote!(number),
191                    NumType::U64 | NumType::I64 => quote!(BigInt),
192                },
193                AbiType::Isize | AbiType::Usize => quote!(number),
194                AbiType::Bool => quote!(boolean),
195                AbiType::RefStr | AbiType::String => quote!(string),
196                AbiType::RefSlice(prim) | AbiType::Vec(prim) => {
197                    let inner = self.generate_return_type(Some(&AbiType::Num(*prim)));
198                    quote!(Array<#inner>)
199                }
200                AbiType::RefObject(i) | AbiType::Object(i) => {
201                    quote!(#(self.type_ident(i)))
202                }
203                AbiType::Option(i) => {
204                    let inner = self.generate_return_type(Some(i));
205                    quote!(#inner?)
206                }
207                AbiType::Result(i) => quote!(#(self.generate_return_type(Some(i)))),
208                AbiType::RefIter(i) | AbiType::Iter(i) => {
209                    let inner = self.generate_return_type(Some(i));
210                    quote!(Iterable<#inner>)
211                }
212                AbiType::RefFuture(i) | AbiType::Future(i) => {
213                    let inner = self.generate_return_type(Some(i));
214                    quote!(Promise<#inner>)
215                }
216                AbiType::RefStream(i) | AbiType::Stream(i) => {
217                    let inner = self.generate_return_type(Some(i));
218                    quote!(ReadableStream<#inner>)
219                }
220                AbiType::Tuple(tys) => match tys.len() {
221                    0 => quote!(void),
222                    1 => self.generate_return_type(Some(&tys[0])),
223                    _ => {
224                        quote!([#(for ty in tys join (, ) => #(self.generate_return_type(Some(ty))))])
225                    }
226                },
227            }
228        } else {
229            quote!(void)
230        }
231    }
232
233    fn generate_object(&self, obj: AbiObject) -> js::Tokens {
234        quote! {
235            export class #(self.type_ident(&obj.name)) {
236                #(for method in obj.methods join (#<line>#<line>) => #(self.generate_function(method)))
237
238                drop(): void;
239            }
240        }
241    }
242
243    fn type_ident(&self, s: &str) -> String {
244        sanitize_identifier(&s.to_upper_camel_case())
245    }
246
247    fn ident(&self, s: &str) -> String {
248        sanitize_identifier(&s.to_lower_camel_case())
249    }
250}
251
252impl JsGenerator {
253    pub fn generate(&self, iface: Interface) -> js::Tokens {
254        quote! {
255            #(static_literal("//")) AUTO GENERATED FILE, DO NOT EDIT.
256            #(static_literal("//"))
257            #(static_literal("//")) Generated by "ffi-gen".
258            #(static_literal("/* tslint:disable */"))
259            #(static_literal("/* eslint:disable */"))
260
261            // a node fetch polyfill that won't trigger webpack or other bundlers
262            // idea borrowed from:
263            // https://github.com/dcodeIO/webassembly/blob/master/src/index.js#L223
264            let fs;
265            const fetch_polyfill = async (file) => {
266                const readFile = await eval("mport('fs')".replace(/^/, 'i'))
267                    .then(({ readFile }) => readFile);
268                return new Promise((resolve, reject) => {
269                    readFile(
270                        file,
271                        function(err, data) {
272                            return (err)
273                                ? reject(err)
274                                : resolve({
275                                    arrayBuffer: () => Promise.resolve(data),
276                                    ok: true,
277                                });
278                        }
279                        );
280                });
281            }
282
283            let ReadableStream;
284            if (typeof window == "object") {
285                ReadableStream = window.ReadableStream;
286                #(static_literal("// Workaround for combined use with `wasm-bindgen`, so we don't have to"))
287                #(static_literal("// patch the `importObject` while loading the WASM module."))
288                window.__notifier_callback = (idx) => notifierRegistry.callbacks[idx]();
289            } else {
290                eval("mport('node:stream/web')".replace(/^/, 'i')).then(pkg => {
291                    ReadableStream = pkg.ReadableStream;
292                });
293                #(static_literal("// Workaround for combined use with `wasm-bindgen`, so we don't have to"))
294                #(static_literal("// patch the `importObject` while loading the WASM module."))
295                global.__notifier_callback = (idx) => notifierRegistry.callbacks[idx]();
296            };
297
298            const fetchFn = (typeof fetch === "function" && fetch) || fetch_polyfill;
299
300            // gets the wasm at a url and instantiates it.
301            // checks if streaming instantiation is available and uses that
302            function fetchAndInstantiate(url, imports) {
303                const env = imports.env || {};
304                env.__notifier_callback = (idx) => notifierRegistry.callbacks[idx]();
305                imports.env = env;
306                return fetchFn(url)
307                    .then((resp) => {
308                        if (!resp.ok) {
309                            throw new Error("Got a ${resp.status} fetching wasm @ ${url}");
310                        }
311
312                        const wasm = "application/wasm";
313                        const type = resp.headers && resp.headers.get("content-type");
314
315                        return (WebAssembly.instantiateStreaming && type === wasm)
316                            ? WebAssembly.instantiateStreaming(resp, imports)
317                            : resp.arrayBuffer().then(buf => WebAssembly.instantiate(buf, imports));
318                        })
319                        .then(result => result.instance);
320            }
321
322            const dropRegistry = new FinalizationRegistry(drop => drop());
323
324            class Box {
325                constructor(ptr, destructor) {
326                    this.ptr = ptr;
327                    this.dropped = false;
328                    this.moved = false;
329                    dropRegistry.register(this, destructor, this);
330                    this.destructor = destructor;
331                }
332
333                borrow() {
334                    if (this.dropped) {
335                        throw new Error("use after free");
336                    }
337                    if (this.moved) {
338                        throw new Error("use after move");
339                    }
340                    return this.ptr;
341                }
342
343                move() {
344                    if (this.dropped) {
345                        throw new Error("use after free");
346                    }
347                    if (this.moved) {
348                        throw new Error("can't move value twice");
349                    }
350                    this.moved = true;
351                    dropRegistry.unregister(this);
352                    return this.ptr;
353                }
354
355                drop() {
356                    if (this.dropped) {
357                        throw new Error("double free");
358                    }
359                    if (this.moved) {
360                        throw new Error("can't drop moved value");
361                    }
362                    this.dropped = true;
363                    dropRegistry.unregister(this);
364                    this.destructor();
365                }
366            }
367
368            class NotifierRegistry {
369                constructor() {
370                    this.counter = 0;
371                    this.callbacks = {};
372                }
373
374                reserveSlot() {
375                    const idx = this.counter;
376                    this.counter += 1;
377                    return idx;
378                }
379
380                registerNotifier(idx, notifier) {
381                    this.callbacks[idx] = notifier;
382                }
383
384                unregisterNotifier(idx) {
385                    delete this.callbacks[idx];
386                }
387            }
388
389            const notifierRegistry = new NotifierRegistry();
390
391            const nativeFuture = (box, nativePoll) => {
392                const poll = (resolve, reject, idx) => {
393                    try {
394                        const ret = nativePoll(box.borrow(), 0, BigInt(idx));
395                        if (ret == null) {
396                            return;
397                        }
398                        resolve(ret);
399                    } catch(err) {
400                        reject(err);
401                    }
402                    notifierRegistry.unregisterNotifier(idx);
403                    box.drop();
404                };
405                return new Promise((resolve, reject) => {
406                    const idx = notifierRegistry.reserveSlot();
407                    const notifier = () => poll(resolve, reject, idx);
408                    notifierRegistry.registerNotifier(idx, notifier);
409                    poll(resolve, reject, idx);
410                });
411            };
412
413            function* nativeIter(box, nxt) {
414                let el;
415                while(true) {
416                    el = nxt(box.borrow());
417                    if (el === null) {
418                        break;
419                    }
420                    yield el;
421                }
422                box.drop();
423            }
424
425            const nativeStream = (box, nativePoll) => {
426                const poll = (next, nextIdx, doneIdx) => {
427                    const ret = nativePoll(box.borrow(), 0, BigInt(nextIdx), BigInt(doneIdx));
428                    if (ret != null) {
429                        next(ret);
430                    }
431                };
432                return new ReadableStream({
433                    start(controller) {
434                        const nextIdx = notifierRegistry.reserveSlot();
435                        const doneIdx = notifierRegistry.reserveSlot();
436                        const nextNotifier = () => setTimeout(() =>
437                            poll(x => controller.enqueue(x), nextIdx, doneIdx),
438                            0);
439                        const doneNotifier = () => {
440                            notifierRegistry.unregisterNotifier(nextIdx);
441                            notifierRegistry.unregisterNotifier(doneIdx);
442                            controller.close();
443                            box.drop();
444                        };
445                        notifierRegistry.registerNotifier(nextIdx, nextNotifier);
446                        notifierRegistry.registerNotifier(doneIdx, doneNotifier);
447                        nextNotifier();
448                    },
449                });
450            };
451
452            export class Api {
453                async fetch(url, imports) {
454                    this.instance = await fetchAndInstantiate(url, imports);
455                }
456
457                initWithInstance(instance) {
458                    this.instance = instance;
459                }
460
461                allocate(size, align) {
462                    return this.instance.exports.allocate(size, align);
463                }
464
465                deallocate(ptr, size, align) {
466                    this.instance.exports.deallocate(ptr, size, align);
467                }
468
469                drop(symbol, ptr) {
470                    this.instance.exports[symbol](0, ptr);
471                }
472
473                #(for func in iface.functions() => #(self.generate_function(&func)))
474                #(for iter in iface.iterators() => #(self.generate_function(&iter.next())))
475                #(for fut in iface.futures() => #(self.generate_function(&fut.poll())))
476                #(for stream in iface.streams() => #(self.generate_function(&stream.poll())))
477            }
478
479            #(for obj in iface.objects() => #(self.generate_object(obj)))
480
481            export default Api;
482        }
483    }
484
485    fn generate_object(&self, obj: AbiObject) -> js::Tokens {
486        quote! {
487            export class #(self.type_ident(&obj.name)) {
488                constructor(api, box) {
489                    this.api = api;
490                    this.box = box;
491                }
492
493                #(for method in obj.methods => #(self.generate_function(&method)))
494
495                drop() {
496                    this.box.drop();
497                }
498            }
499        }
500    }
501
502    fn generate_function(&self, func: &AbiFunction) -> js::Tokens {
503        let ffi = self.abi.import(func);
504        let api = match &func.ty {
505            FunctionType::Constructor(_) => quote!(api),
506            FunctionType::Method(_) => quote!(this.api),
507            FunctionType::Function
508            | FunctionType::NextIter(_, _)
509            | FunctionType::PollFuture(_, _)
510            | FunctionType::PollStream(_, _) => quote!(this),
511        };
512        let func_name = self.ident(match &func.ty {
513            FunctionType::PollFuture(_, _)
514            | FunctionType::PollStream(_, _)
515            | &FunctionType::NextIter(_, _) => &ffi.symbol,
516            _ => &func.name,
517        });
518        let args = quote!(#(for (name, _) in &ffi.abi_args => #(self.ident(name)),));
519        let body = quote!(#(for instr in &ffi.instr => #(self.generate_instr(&api, instr))));
520        match &func.ty {
521            FunctionType::Constructor(_) => quote! {
522                static #(self.ident(&func.name))(api, #args) {
523                    #body
524                }
525            },
526            _ => quote! {
527                #func_name(#args) {
528                    #body
529                }
530            },
531        }
532    }
533
534    fn generate_instr(&self, api: &js::Tokens, instr: &Instr) -> js::Tokens {
535        match instr {
536            Instr::BorrowSelf(out) => quote!(#(self.var(out)) = this.box.borrow();),
537            Instr::BorrowObject(in_, out)
538            | Instr::BorrowIter(in_, out)
539            | Instr::BorrowFuture(in_, out)
540            | Instr::BorrowStream(in_, out) => {
541                quote!(#(self.var(out)) = #(self.var(in_)).box.borrow();)
542            }
543            Instr::MoveObject(in_, out)
544            | Instr::MoveIter(in_, out)
545            | Instr::MoveFuture(in_, out)
546            | Instr::MoveStream(in_, out) => {
547                quote!(#(self.var(out)) = #(self.var(in_)).box.move();)
548            }
549            Instr::LiftObject(obj, box_, drop, out) => quote! {
550                const #(self.var(box_))_0 = () => { #api.drop(#_(#drop), #(self.var(box_))); };
551                const #(self.var(box_))_1 = new Box(#(self.var(box_)), #(self.var(box_))_0);
552                const #(self.var(out)) = new #obj(#api, #(self.var(box_))_1);
553            },
554            Instr::BindArg(arg, out) => quote!(const #(self.var(out)) = #(self.ident(arg));),
555            Instr::BindRets(ret, vars) => match vars.len() {
556                0 => quote!(),
557                1 => quote!(const #(self.var(&vars[0])) = #(self.var(ret));),
558                _ => quote! {
559                    #(for (idx, var) in vars.iter().enumerate() =>
560                        const #(self.var(var)) = #(self.var(ret))[#(idx)];)
561                },
562            },
563            Instr::LiftNumFromU32Tuple(low, high, out, num_type) => {
564                let arr = match num_type {
565                    NumType::U64 => quote!(BigUint64Array),
566                    NumType::I64 => quote!(BigInt64Array),
567                    _ => unreachable!(),
568                };
569                quote! {
570                    const #(self.var(out))_0 = new Uint32Array(2);
571                    #(self.var(out))_0[0] = #(self.var(low));
572                    #(self.var(out))_0[1] = #(self.var(high));
573                    const #(self.var(out)) = new #(arr)(#(self.var(out))_0.buffer)[0];
574                }
575            }
576            Instr::LiftNum(r#in, out, NumType::U32) => {
577                quote!(const #(self.var(out)) = #(self.var(r#in)) >>> 0;)
578            }
579            Instr::LowerNumFromU32Tuple(r#in, out_low, out_high, num_type) => {
580                let arr = match num_type {
581                    NumType::U64 => quote!(BigUint64Array),
582                    NumType::I64 => quote!(BigInt64Array),
583                    _ => unreachable!(),
584                };
585                quote! {
586                    const #(self.var(out_low))_0 = new #(arr)(1);
587                    #(self.var(out_low))_0[0] = #(self.var(r#in));
588                    const #(self.var(out_low))_1 = new Uint32Array(#(self.var(out_low))_0.buffer);
589                    #(self.var(out_low)) = #(self.var(out_low))_1[0];
590                    #(self.var(out_high)) = #(self.var(out_low))_1[1];
591                }
592            }
593            Instr::LowerNum(in_, out, _num) => {
594                quote!(#(self.var(out)) = #(self.var(in_));)
595            }
596            Instr::LiftNum(in_, out, _num) => {
597                quote!(const #(self.var(out)) = #(self.var(in_));)
598            }
599            Instr::LowerBool(in_, out) => {
600                quote!(#(self.var(out)) = #(self.var(in_)) ? 1 : 0;)
601            }
602            Instr::LiftBool(in_, out) => quote!(const #(self.var(out)) = #(self.var(in_)) > 0;),
603            Instr::Deallocate(ptr, len, size, align) => quote! {
604                if (#(self.var(len)) > 0) {
605                    #api.deallocate(#(self.var(ptr)), #(self.var(len)) * #(*size), #(*align));
606                }
607            },
608            Instr::LowerString(in_, ptr, len, cap, size, align) => quote! {
609                const #(self.var(in_))_0 = new TextEncoder();
610                const #(self.var(in_))_1 = #(self.var(in_))_0.encode(#(self.var(in_)));
611                #(self.var(len)) = #(self.var(in_))_1.length;
612                #(self.var(ptr)) = #api.allocate(#(self.var(len)) * #(*size), #(*align));
613                const #(self.var(ptr))_0 =
614                    new Uint8Array(#api.instance.exports.memory.buffer, #(self.var(ptr)), #(self.var(len)));
615                #(self.var(ptr))_0.set(#(self.var(in_))_1, 0);
616                #(self.var(cap)) = #(self.var(len));
617            },
618            Instr::LiftString(ptr, len, out) => quote! {
619                const #(self.var(out))_0 =
620                    new Uint8Array(#api.instance.exports.memory.buffer, #(self.var(ptr)), #(self.var(len)));
621                const #(self.var(out))_1 = new TextDecoder();
622                const #(self.var(out)) = #(self.var(out))_1.decode(#(self.var(out))_0);
623            },
624            Instr::LowerVec(in_, ptr, len, cap, ty, size, align) => quote! {
625                #(self.var(len)) = #(self.var(in_)).length;
626                #(self.var(ptr)) = #api.allocate(#(self.var(len)) * #(*size), #(*align));
627                const #(self.var(ptr))_0 =
628                    new #(self.generate_array(*ty))(
629                        #api.instance.exports.memory.buffer, #(self.var(ptr)), #(self.var(len)));
630                #(self.var(ptr))_0.set(#(self.var(in_)), 0);
631                #(self.var(cap)) = #(self.var(len));
632            },
633            Instr::LiftVec(ptr, len, out, ty) => quote! {
634                const #(self.var(out))_0 =
635                    new #(self.generate_array(*ty))(
636                        #api.instance.exports.memory.buffer, #(self.var(ptr)), #(self.var(len)));
637                const #(self.var(out)) = Array.from(#(self.var(out))_0);
638            },
639            Instr::Call(symbol, ret, args) => {
640                let invoke =
641                    quote!(#api.instance.exports.#symbol(#(for arg in args => #(self.var(arg)),)););
642                if let Some(ret) = ret {
643                    quote!(const #(self.var(ret)) = #invoke)
644                } else {
645                    invoke
646                }
647            }
648            Instr::DefineArgs(vars) => quote! {
649                #(for var in vars => let #(self.var(var)) = 0;)
650            },
651            Instr::ReturnValue(ret) => quote!(return #(self.var(ret));),
652            Instr::ReturnVoid => quote!(return;),
653            Instr::HandleNull(var) => quote! {
654                if (#(self.var(var)) === 0) {
655                    return null;
656                }
657            },
658            Instr::LowerOption(arg, var, some, some_instr) => quote! {
659                if (#(self.var(arg)) == null) {
660                    #(self.var(var)) = 0;
661                } else {
662                    #(self.var(var)) = 1;
663                    const #(self.var(some)) = #(self.var(arg));
664                    #(for inst in some_instr => #(self.generate_instr(api, inst)))
665                }
666            },
667            Instr::HandleError(var, ptr, len, cap) => quote! {
668                if (#(self.var(var)) === 0) {
669                    const #(self.var(var))_0 =
670                        new Uint8Array(#api.instance.exports.memory.buffer, #(self.var(ptr)), #(self.var(len)));
671                    const #(self.var(var))_1 = new TextDecoder();
672                    const #(self.var(var))_2 = #(self.var(var))_1.decode(#(self.var(var))_0);
673                    if (#(self.var(len)) > 0) {
674                        #api.deallocate(#(self.var(ptr)), #(self.var(cap)), 1);
675                    }
676                    throw #(self.var(var))_2;
677                }
678            },
679            Instr::LiftIter(box_, next, drop, out) => quote! {
680                const #(self.var(box_))_0 = () => { #api.drop(#_(#drop), #(self.var(box_))); };
681                const #(self.var(box_))_1 = new Box(#(self.var(box_)), #(self.var(box_))_0);
682                const #(self.var(out)) = nativeIter(#(self.var(box_))_1, (a) => {
683                    return #api.#(self.ident(next))(a);
684                });
685            },
686            Instr::LiftFuture(box_, poll, drop, out) => quote! {
687                const #(self.var(box_))_0 = () => { #api.drop(#_(#drop), #(self.var(box_))); };
688                const #(self.var(box_))_1 = new Box(#(self.var(box_)), #(self.var(box_))_0);
689                const #(self.var(out)) = nativeFuture(#(self.var(box_))_1, (a, b, c) => {
690                    return #api.#(self.ident(poll))(a, b, c);
691                });
692            },
693            Instr::LiftStream(box_, poll, drop, out) => quote! {
694                const #(self.var(box_))_0 = () => { #api.drop(#_(#drop), #(self.var(box_))); };
695                const #(self.var(box_))_1 = new Box(#(self.var(box_)), #(self.var(box_))_0);
696                const #(self.var(out)) = nativeStream(#(self.var(box_))_1, (a, b, c, d) => {
697                    return #api.#(self.ident(poll))(a, b, c, d);
698                });
699            },
700            Instr::LiftTuple(vars, out) => match vars.len() {
701                0 => quote!(),
702                1 => quote!(const #(self.var(out)) = #(self.var(&vars[0]));),
703                _ => quote! {
704                    const #(self.var(out)) = [];
705                    #(for var in vars => #(self.var(out)).push(#(self.var(var)));)
706                },
707            },
708        }
709    }
710
711    fn var(&self, var: &Var) -> js::Tokens {
712        quote!(#(format!("tmp{}", var.binding)))
713    }
714
715    fn generate_array(&self, ty: NumType) -> js::Tokens {
716        match ty {
717            NumType::U8 => quote!(Uint8Array),
718            NumType::U16 => quote!(Uint16Array),
719            NumType::U32 => quote!(Uint32Array),
720            NumType::U64 => quote!(BigUint64Array),
721            NumType::I8 => quote!(Int8Array),
722            NumType::I16 => quote!(Int16Array),
723            NumType::I32 => quote!(Int32Array),
724            NumType::I64 => quote!(BigInt64Array),
725            NumType::F32 => quote!(Float32Array),
726            NumType::F64 => quote!(Float64Array),
727        }
728    }
729
730    fn type_ident(&self, s: &str) -> String {
731        sanitize_identifier(&s.to_upper_camel_case())
732    }
733
734    fn ident(&self, s: &str) -> String {
735        sanitize_identifier(&s.to_lower_camel_case())
736    }
737}
738
739pub struct WasmMultiValueShim {
740    abi: Abi,
741}
742
743impl Default for WasmMultiValueShim {
744    fn default() -> Self {
745        Self::new()
746    }
747}
748
749impl WasmMultiValueShim {
750    pub fn new() -> Self {
751        Self { abi: Abi::Wasm32 }
752    }
753
754    #[cfg(feature = "test_runner")]
755    pub fn generate(&self, path: &str, iface: Interface) -> rust::Tokens {
756        let args = self.generate_args(iface);
757        if !args.is_empty() {
758            quote! {
759                let ret = Command::new("multi-value-reverse-polyfill")
760                    .arg(#_(#path))
761                    #(for arg in args => .arg(#(quoted(arg))))
762                    .status()
763                    .unwrap()
764                    .success();
765                assert!(ret);
766            }
767        } else {
768            quote! {
769                let ret = Command::new("cp")
770                    .arg(#_(#path))
771                    .arg(#_(#(path).multivalue.wasm))
772                    .status()
773                    .unwrap()
774                    .success();
775                assert!(ret);
776            }
777        }
778    }
779
780    pub fn run<P: AsRef<Path>>(&self, path: P, iface: Interface) -> Result<()> {
781        let args = self.generate_args(iface);
782        let path = path.as_ref().to_str().unwrap();
783        if !args.is_empty() {
784            let mut cmd = Command::new("multi-value-reverse-polyfill");
785            cmd.arg(path);
786            for arg in args {
787                cmd.arg(arg);
788            }
789            let status = cmd.status()?;
790            if !status.success() {
791                anyhow::bail!("multi-value-reverse-polyfill failed for: {:?}", cmd);
792            }
793        } else {
794            let status = Command::new("cp")
795                .arg(path)
796                .arg(format!("{}.multivalue.wasm", path))
797                .status()?;
798            if !status.success() {
799                anyhow::bail!("cp failed");
800            }
801        }
802        Ok(())
803    }
804
805    fn generate_args(&self, iface: Interface) -> Vec<String> {
806        iface
807            .imports(&self.abi)
808            .into_iter()
809            .filter_map(|import| match &import.ffi_ret {
810                Return::Struct(fields, _) => {
811                    let mut ret = String::new();
812                    for field in fields {
813                        let (size, _) = self.abi.layout(field.ty.num());
814                        let r = match field.ty.num() {
815                            NumType::F32 => "f32 ",
816                            NumType::F64 => "f64 ",
817                            _ if size > 4 => "i64 ",
818                            _ => "i32 ",
819                        };
820                        ret.push_str(r);
821                    }
822
823                    Some(format!("{} {}", import.symbol, ret))
824                }
825                _ => None,
826            })
827            .collect()
828    }
829}
830
831#[cfg(feature = "test_runner")]
832#[doc(hidden)]
833pub mod test_runner {
834    use super::*;
835    use crate::{Abi, RustGenerator};
836    use anyhow::Result;
837    use std::io::Write;
838    use tempfile::NamedTempFile;
839    use trybuild::TestCases;
840
841    pub fn compile_pass(iface: &str, rust: rust::Tokens, js: js::Tokens) -> Result<()> {
842        let iface = Interface::parse(iface)?;
843        let mut rust_file = NamedTempFile::new()?;
844        let rust_gen = RustGenerator::new(Abi::Wasm32);
845        let rust_tokens = rust_gen.generate(iface.clone());
846        let mut js_file = tempfile::Builder::new().suffix(".mjs").tempfile()?;
847        let js_gen = JsGenerator::default();
848        let js_tokens = js_gen.generate(iface.clone());
849
850        let library_tokens = quote! {
851            #rust_tokens
852            #rust
853
854            extern "C" {
855                fn __panic(ptr: isize, len: usize);
856                fn __log(ptr: isize, len: usize);
857            }
858
859            pub fn panic(msg: &str) {
860                unsafe { __panic(msg.as_ptr() as _, msg.len()) };
861            }
862
863            pub fn log(msg: &str) {
864                unsafe { __log(msg.as_ptr() as _, msg.len()) };
865            }
866        };
867
868        let library_file = NamedTempFile::new()?;
869        let bin_tokens = quote! {
870            import assert from "assert";
871            #js_tokens
872
873            async function main() {
874                const api = new Api();
875                await api.fetch(#_(#(library_file.as_ref().to_str().unwrap()).multivalue.wasm), {
876                    env: {
877                        __panic: (ptr, len) => {
878                            const buf = new Uint8Array(api.instance.exports.memory.buffer, ptr, len);
879                            const decoder = new TextDecoder();
880                            throw decoder.decode(buf);
881                        },
882                        __log: (ptr, len) => {
883                            const buf = new Uint8Array(api.instance.exports.memory.buffer, ptr, len);
884                            const decoder = new TextDecoder();
885                            console.log(decoder.decode(buf));
886                        },
887                    }
888                });
889                #js
890            }
891            main();
892        };
893
894        let library = library_tokens.to_file_string()?;
895        rust_file.write_all(library.as_bytes())?;
896        let bin = bin_tokens.to_file_string()?;
897        js_file.write_all(bin.as_bytes())?;
898
899        let wasm_multi_value =
900            WasmMultiValueShim::new().generate(library_file.as_ref().to_str().unwrap(), iface);
901
902        let runner_tokens: rust::Tokens = quote! {
903            fn main() {
904                use std::process::Command;
905                let ret = Command::new("rustc")
906                    .arg("--edition")
907                    .arg("2021")
908                    .arg("--crate-name")
909                    .arg("compile_pass")
910                    .arg("--crate-type")
911                    .arg("cdylib")
912                    .arg("-o")
913                    .arg(#(quoted(library_file.as_ref().to_str().unwrap())))
914                    .arg("--cfg")
915                    .arg("feature=\"test_runner\"")
916                    .arg("--target")
917                    .arg("wasm32-unknown-unknown")
918                    .arg(#(quoted(rust_file.as_ref().to_str().unwrap())))
919                    .status()
920                    .expect("Compiling lib")
921                    .success();
922                assert!(ret);
923                //println!("{}", #_(#bin));
924                #wasm_multi_value
925                let ret = Command::new("node")
926                    .arg("--expose-gc")
927                    .arg("--unhandled-rejections=strict")
928                    .arg(#(quoted(js_file.as_ref().to_str().unwrap())))
929                    .status()
930                    .expect("Running node")
931                    .success();
932                assert!(ret);
933            }
934        };
935
936        let mut runner_file = NamedTempFile::new()?;
937        let runner = runner_tokens.to_file_string()?;
938        runner_file.write_all(runner.as_bytes())?;
939
940        //        js_file.keep()?;
941        //        rust_file.keep()?;
942        //        library_file.keep()?;
943        //        let (_, p) = runner_file.keep()?;
944        let test = TestCases::new();
945        //        test.pass(p);
946        test.pass(runner_file.as_ref());
947        Ok(())
948    }
949
950    pub fn compile_pass_ts(iface: &str, ts_tokens: js::Tokens) -> Result<()> {
951        let iface = Interface::parse(iface)?;
952        let ts_gen = TsGenerator { docs: false };
953        let js_tokens = ts_gen.generate(iface);
954        // remove static header to no bloat the tests
955        let left = js_tokens.to_file_string().unwrap().replace(
956            r#"// AUTO GENERATED FILE, DO NOT EDIT.
957//
958// Generated by "ffi-gen".
959/* tslint:disable */
960/* eslint:disable */
961
962"#,
963            "",
964        );
965        let right = ts_tokens.to_file_string().unwrap();
966
967        assert_eq!(left, right, "left: {}\nright: {}", left, right);
968        Ok(())
969    }
970}