fp_bindgen/generators/
mod.rs

1use crate::{
2    functions::FunctionList,
3    types::{CargoDependency, Type, TypeIdent, TypeMap},
4};
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    fmt::Display,
8    fs,
9};
10
11pub mod rust_plugin;
12pub mod rust_wasmer2_runtime;
13pub mod rust_wasmer2_wasi_runtime;
14pub mod ts_runtime;
15
16#[non_exhaustive]
17#[derive(Debug, Clone)]
18pub enum BindingsType {
19    RustPlugin(RustPluginConfig),
20    RustWasmer2Runtime,
21    RustWasmer2WasiRuntime,
22    TsRuntime(TsRuntimeConfig),
23}
24
25impl Display for BindingsType {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(match self {
28            BindingsType::RustPlugin { .. } => "rust-plugin",
29            BindingsType::RustWasmer2Runtime { .. } => "rust-wasmer2-runtime",
30            BindingsType::RustWasmer2WasiRuntime { .. } => "rust-wasmer2-wasi-runtime",
31            BindingsType::TsRuntime { .. } => "ts-runtime",
32        })
33    }
34}
35
36#[derive(Debug)]
37pub struct BindingConfig<'a> {
38    pub bindings_type: BindingsType,
39    pub path: &'a str,
40}
41
42#[non_exhaustive]
43#[derive(Debug, Clone)]
44pub struct RustPluginConfig {
45    /// Name of the plugin crate that will be generated.
46    pub name: Option<RustPluginConfigValue>,
47
48    /// Authors to be listed in the plugin crate that will be generated.
49    pub authors: Option<RustPluginConfigValue>,
50
51    /// Version of the plugin crate that will be generated.
52    pub version: Option<RustPluginConfigValue>,
53
54    /// *Additional* dependencies to be listed in the plugin crate that will be
55    /// generated.
56    ///
57    /// These are merged with a small set of dependencies that are necessary
58    /// for the plugin to work and which will always be included. Specifying
59    /// these dependencies yourself can be useful if you want to explicitly bump
60    /// a dependency version or you want to enable a Cargo feature in them.
61    pub dependencies: BTreeMap<String, CargoDependency>,
62
63    /// The human-readable description for the generated crate.
64    pub description: Option<RustPluginConfigValue>,
65
66    /// A readme file containing some information for the generated crate.
67    pub readme: Option<RustPluginConfigValue>,
68
69    /// The license of the generated crate.
70    pub license: Option<RustPluginConfigValue>,
71}
72
73impl RustPluginConfig {
74    pub fn builder() -> RustPluginConfigBuilder {
75        RustPluginConfigBuilder {
76            config: RustPluginConfig {
77                name: None,
78                authors: None,
79                version: None,
80                dependencies: Default::default(),
81                description: None,
82                readme: None,
83                license: None,
84            },
85        }
86    }
87}
88
89#[derive(Debug, Clone)]
90#[non_exhaustive]
91pub enum RustPluginConfigValue {
92    String(String),
93    Vec(Vec<String>),
94    Workspace,
95}
96
97impl From<&str> for RustPluginConfigValue {
98    fn from(value: &str) -> Self {
99        Self::String(value.into())
100    }
101}
102
103impl From<String> for RustPluginConfigValue {
104    fn from(value: String) -> Self {
105        Self::String(value)
106    }
107}
108
109impl From<Vec<&str>> for RustPluginConfigValue {
110    fn from(value: Vec<&str>) -> Self {
111        Self::Vec(value.into_iter().map(|value| value.to_string()).collect())
112    }
113}
114
115impl From<Vec<String>> for RustPluginConfigValue {
116    fn from(value: Vec<String>) -> Self {
117        Self::Vec(value)
118    }
119}
120
121pub struct RustPluginConfigBuilder {
122    config: RustPluginConfig,
123}
124
125impl RustPluginConfigBuilder {
126    pub fn name(mut self, value: impl Into<String>) -> Self {
127        self.config.name = Some(RustPluginConfigValue::String(value.into()));
128        self
129    }
130
131    pub fn version(mut self, value: impl Into<RustPluginConfigValue>) -> Self {
132        self.config.version = Some(value.into());
133        self
134    }
135
136    pub fn authors(mut self, value: impl Into<RustPluginConfigValue>) -> Self {
137        self.config.authors = Some(value.into());
138        self
139    }
140
141    pub fn author(mut self, value: impl Into<String>) -> Self {
142        match &mut self.config.authors {
143            Some(RustPluginConfigValue::Vec(vec)) => {
144                vec.push(value.into());
145            }
146            None => {
147                self.config.authors = Some(RustPluginConfigValue::Vec(vec![value.into()]));
148            }
149            _ => panic!("Cannot add an author to a non-vector 'authors' field"),
150        }
151        self
152    }
153
154    pub fn description(mut self, value: impl Into<RustPluginConfigValue>) -> Self {
155        self.config.description = Some(value.into());
156        self
157    }
158
159    pub fn readme(mut self, value: impl Into<RustPluginConfigValue>) -> Self {
160        self.config.readme = Some(value.into());
161        self
162    }
163
164    pub fn license(mut self, value: impl Into<RustPluginConfigValue>) -> Self {
165        self.config.license = Some(value.into());
166        self
167    }
168
169    pub fn dependencies<'a>(
170        mut self,
171        value: impl Into<BTreeMap<&'a str, CargoDependency>>,
172    ) -> Self {
173        let dependencies = value.into();
174        self.config.dependencies = dependencies
175            .into_iter()
176            .map(|(key, value)| (key.to_string(), value))
177            .collect();
178        self
179    }
180
181    pub fn dependency(mut self, name: impl Into<String>, dependency: CargoDependency) -> Self {
182        self.config.dependencies.insert(name.into(), dependency);
183        self
184    }
185
186    pub fn build(self) -> RustPluginConfig {
187        assert!(
188            self.config.name.is_some(),
189            "'name' is required in RustPluginConfig"
190        );
191        self.config
192    }
193}
194
195#[non_exhaustive]
196#[derive(Debug, Clone)]
197pub struct TsRuntimeConfig {
198    /// The module from which to import the MessagePack dependency.
199    ///
200    /// By default, "@msgpack/msgpack" is used, which should work with Node.js
201    /// and most NPM-based bundlers. If you use Deno, you may wish to specify
202    /// "https://unpkg.com/@msgpack/msgpack/mod.ts".
203    pub msgpack_module: String,
204
205    /// Whether or not to generate raw export wrappers.
206    ///
207    /// Raw export wrappers allow you to call `fp_export!` functions from the
208    /// runtime while passing raw MessagePack data, which you can use in some
209    /// situations to avoid (de)serialization overhead. If you don't need these
210    /// wrappers, you can omit them to optimize your bundle size.
211    ///
212    /// Raw export wrappers are named similarly to the regular wrappers (which
213    /// are generated in any case), but with a `Raw` suffix.
214    pub generate_raw_export_wrappers: bool,
215
216    /// Use `WebAssembly.instantiateStreaming()` instead of
217    /// `WebAssembly.instantiate()` for optimizing instantiation in browser use
218    /// cases. This changes the signature of the `createRuntime()` function to
219    /// accept a `Response` instead of an `ArrayBuffer`.
220    ///
221    /// See also: https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming
222    ///
223    /// This setting is `true` by default, since MDN recommends it where
224    /// possible. You may wish to opt-out if you're using the runtime in an
225    /// environment that doesn't support streaming instantiation, such as
226    /// Node.js.
227    pub streaming_instantiation: bool,
228}
229
230impl TsRuntimeConfig {
231    /// Returns a new config instance with default settings.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Sets the `msgpack_module` setting.
237    pub fn with_msgpack_module(mut self, msgpack_module: &str) -> Self {
238        self.msgpack_module = msgpack_module.to_owned();
239        self
240    }
241
242    /// Enables the `generate_raw_export_wrappers` setting.
243    pub fn with_raw_export_wrappers(mut self) -> Self {
244        self.generate_raw_export_wrappers = true;
245        self
246    }
247
248    /// Disables the `streaming_instantiation` setting.
249    pub fn without_streaming_instantiation(mut self) -> Self {
250        self.streaming_instantiation = false;
251        self
252    }
253}
254
255impl Default for TsRuntimeConfig {
256    fn default() -> Self {
257        Self {
258            generate_raw_export_wrappers: false,
259            msgpack_module: "@msgpack/msgpack".to_owned(),
260            streaming_instantiation: true,
261        }
262    }
263}
264
265impl TsRuntimeConfig {}
266
267pub fn generate_bindings(
268    import_functions: FunctionList,
269    export_functions: FunctionList,
270    types: TypeMap,
271    config: BindingConfig,
272) {
273    fs::create_dir_all(config.path).expect("Could not create output directory");
274
275    display_warnings(&import_functions, &export_functions, &types);
276
277    match config.bindings_type {
278        BindingsType::RustPlugin(plugin_config) => rust_plugin::generate_bindings(
279            import_functions,
280            export_functions,
281            types,
282            plugin_config,
283            config.path,
284        ),
285        BindingsType::RustWasmer2Runtime => rust_wasmer2_runtime::generate_bindings(
286            import_functions,
287            export_functions,
288            types,
289            config.path,
290        ),
291        BindingsType::RustWasmer2WasiRuntime => rust_wasmer2_wasi_runtime::generate_bindings(
292            import_functions,
293            export_functions,
294            types,
295            config.path,
296        ),
297        BindingsType::TsRuntime(runtime_config) => ts_runtime::generate_bindings(
298            import_functions,
299            export_functions,
300            types,
301            runtime_config,
302            config.path,
303        ),
304    };
305}
306
307fn display_warnings(
308    import_functions: &FunctionList,
309    export_functions: &FunctionList,
310    types: &TypeMap,
311) {
312    let all_functions = import_functions.iter().chain(export_functions.iter());
313    let all_function_signature_types = all_functions.flat_map(|func| {
314        func.args
315            .iter()
316            .map(|arg| &arg.ty)
317            .chain(func.return_type.iter())
318    });
319    warn_about_custom_serializer_usage(
320        all_function_signature_types.clone(),
321        "function signature",
322        types,
323    );
324
325    // Finding usages as generic arguments is more tricky, because we need to
326    // find all places where generic arguments can be used.
327    let all_idents = all_function_signature_types
328        .chain(
329            types
330                .values()
331                .filter_map(|ty| match ty {
332                    Type::Struct(ty) => Some(ty),
333                    _ => None,
334                })
335                .flat_map(|ty| ty.fields.iter().map(|field| &field.ty)),
336        )
337        .chain(
338            types
339                .values()
340                .filter_map(|ty| match ty {
341                    Type::Enum(ty) => Some(ty),
342                    _ => None,
343                })
344                .flat_map(|ty| ty.variants.iter())
345                .filter_map(|variant| match &variant.ty {
346                    Type::Struct(ty) => Some(ty),
347                    _ => None,
348                })
349                .flat_map(|ty| ty.fields.iter().map(|field| &field.ty)),
350        );
351    warn_about_custom_serializer_usage(
352        all_idents.flat_map(|ident| ident.generic_args.iter().map(|(arg, _)| arg)),
353        "generic argument",
354        types,
355    );
356}
357
358fn warn_about_custom_serializer_usage<'a, T>(idents: T, context: &str, types: &TypeMap)
359where
360    T: Iterator<Item = &'a TypeIdent>,
361{
362    let mut idents_with_custom_serializers = BTreeSet::new();
363
364    for ident in idents {
365        let ty = types.get(ident);
366        if let Some(Type::Custom(ty)) = ty {
367            if ty.serde_attrs.iter().any(|attr| {
368                attr.starts_with("with = ")
369                    || attr.starts_with("serialize_with = ")
370                    || attr.starts_with("deserialize_with = ")
371            }) {
372                idents_with_custom_serializers.insert(ident);
373            }
374        }
375    }
376
377    for ident in idents_with_custom_serializers {
378        println!(
379            "WARNING: Type `{ident}` is used directly in a {context}, but relies on a custom Serde \
380            (de)serializer. This (de)serializer is NOT used when using the type directly \
381            in a {context}. This may result in unexpected (de)serialization issues, for instance \
382            when passing data between Rust and TypeScript.\n\
383            You may wish to create a newtype to avoid this warning.\n\
384            See `examples/example-protocol/src/types/time.rs` for an example."
385        );
386    }
387}