dodo-bindgen-codegen 2.0.0

TypeScript codegen for dodo_bindgen
Documentation
use proc_macro2::TokenStream;
use syn::Ident;

use crate::util::*;

/* -------------------------------------------------------------------------- */

// MARK: api
// https://docs.deno.com/runtime/reference/deno_namespace_apis/#supported-types
#[derive(Clone, Debug, PartialEq)]
pub enum FfiType {
    U8,
    U16,
    U32,
    U64,
    Usize,
    I8,
    I16,
    I32,
    I64,
    Isize,
    F32,
    F64,
    Void,
    Bool,
    Pointer,
    Buffer, /* cannot be constructed directly. must be passed through the utility modules for
             * conversion from opaque pointer to access internal buffer */
    FnPointer,
    // Struct, // [!TODO] support for struct type
}

// https://docs.deno.com/api/deno/~/Deno.ForeignFunction
#[derive(Clone, Debug, PartialEq)]
pub struct FfiFunction {
    pub ident: Ident,
    pub inputs: Vec<FfiType>,
    pub output: FfiType,
    pub non_blocking: bool, /* [!ISSUE] how to prevent data races and enforce integrity
                             * pub optional:     bool, */
}

// https://docs.deno.com/api/deno/~/Deno.ForeignLibraryInterface
#[derive(Clone, Debug, PartialEq)]
pub enum FfiSymbol {
    Function(FfiFunction),
    // Static(FfiStatic),
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct FfiInterface {
    pub symbols: Vec<FfiSymbol>,
}

impl FfiInterface {
    pub fn push_fn(&mut self, ffi_function: FfiFunction) {
        self.symbols.push(FfiSymbol::Function(ffi_function));
    }
    pub fn append(&mut self, other: &mut FfiInterface) {
        self.symbols.append(&mut other.symbols);
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct FfiLib {
    pub interface: FfiInterface,
    pub lib_name: String,
    pub lazy: bool,
    pub export: bool,
}

impl FfiLib {
    pub fn file_name(lib_name: &str, target: &str, extension: &str) -> String {
        format!("{lib_name}.{target}.{extension}")
    }
}

/* -------------------------------------------------------------------------- */

// MARK: print

#[rustfmt::skip]
impl ToTokens for FfiType {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.extend(match self {
            FfiType::U8        => quote! { "u8" },
            FfiType::U16       => quote! { "u16" },
            FfiType::U32       => quote! { "u32" },
            FfiType::U64       => quote! { "u64" },
            FfiType::Usize     => quote! { "usize" },
            FfiType::I8        => quote! { "i8" },
            FfiType::I16       => quote! { "i16" },
            FfiType::I32       => quote! { "i32" },
            FfiType::I64       => quote! { "i64" },
            FfiType::Isize     => quote! { "isize" },
            FfiType::F32       => quote! { "f32" },
            FfiType::F64       => quote! { "f64" },
            FfiType::Void      => quote! { "void" },
            FfiType::Bool      => quote! { "bool" },
            FfiType::Pointer   => quote! { "pointer" },
            FfiType::Buffer    => quote! { "buffer" },
            FfiType::FnPointer => quote! { "function" },
        });
    }
}

impl ToTokens for FfiFunction {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let FfiFunction {
            ident,
            inputs,
            output,
            non_blocking,
        } = self;

        let inputs: Vec<&FfiType> = inputs
            .iter()
            .filter(|ffi_type| {
                if **ffi_type == FfiType::Void {
                    false
                } else {
                    true
                }
            })
            .collect();

        let inputs = quote! { #(#inputs),* };
        let non_blocking = if *non_blocking {
            quote! { nonblocking: true }
        } else {
            TokenStream::new()
        };

        tokens.extend(quote! {
            #ident: {
                parameters: [#inputs],
                result: #output,
                #non_blocking
            }
        });
    }
}

impl ToTokens for FfiSymbol {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.extend(match self {
            FfiSymbol::Function(ffi_function) => ffi_function.to_token_stream(),
        });
    }
}

impl ToTokens for FfiInterface {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let symbols = &self.symbols;
        tokens.extend(quote! {
            { #(#symbols),* }
        });
    }
}

impl<'a> ToTokens for FfiLib {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let FfiLib {
            interface,
            lib_name,
            lazy,
            export,
        } = self;

        let export = if *export {
            quote! { export }
        } else {
            TokenStream::new()
        };

        let file_name = FfiLib::file_name(
            lib_name,
            "${Deno.build.target}",
            "${({ windows: \"dll\", darwin: \"dylib\" } as Record<string, string>)[Deno.build.os] ?? \"so\"}",
        );
        let lib_path = raw(format!(
            "const __dodo_lib = new URL(`./{file_name}`, import.meta.url);"
        ));

        tokens.extend(if *lazy {
            // [!TODO] refactor to a class?
            quote! {
                #lib_path
                let symbols: any;
                export function load(path: string | URL = __dodo_lib) {
                    const { dlopen } = Deno;
                    const { symbols: symbols_ } = dlopen(path, #interface);
                    symbols = symbols_;
                };
            }
        } else {
            quote! {
                #lib_path
                #export const { symbols } = Deno.dlopen(__dodo_lib, #interface);
            }
        });
    }
}

/* -------------------------------------------------------------------------- */

// MARK: tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_print() {
        let mut ffi_lib = FfiLib {
            lib_name: "lib".to_string(),
            export: true,
            ..FfiLib::default()
        };
        ffi_lib.interface.push_fn(FfiFunction {
            ident: format_ident!("some_symbol"),
            inputs: vec![FfiType::Pointer, FfiType::U8],
            output: FfiType::Void,
            non_blocking: false,
        });
        ffi_lib.interface.push_fn(FfiFunction {
            ident: format_ident!("slow_symbol"),
            inputs: vec![FfiType::Buffer],
            output: FfiType::U64,
            non_blocking: true,
        });
        let eager = ffi_lib.to_token_stream().to_string();
        ffi_lib.lazy = true;
        let lazy = ffi_lib.to_token_stream().to_string();
        insta::assert_snapshot!(crate::util::deno_fmt(format!("{eager}\n{lazy}")));
    }
}