Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for traits #2871

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions crates/backend/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct Program {
pub enums: Vec<Enum>,
/// rust structs
pub structs: Vec<Struct>,
/// rust traits
pub traits: Vec<Trait>,
/// custom typescript sections to be included in the definition file
pub typescript_custom_sections: Vec<String>,
/// Inline JS snippets
Expand All @@ -36,6 +38,7 @@ impl Program {
&& self.structs.is_empty()
&& self.typescript_custom_sections.is_empty()
&& self.inline_js.is_empty()
&& self.traits.is_empty()
}
}

Expand Down Expand Up @@ -379,6 +382,44 @@ pub struct Variant {
pub comments: Vec<String>,
}

/// Information about a Trait being exported
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
#[derive(Clone)]
pub struct Trait {
/// The name of the trait in Rust code
pub rust_name: Ident,
/// The name of the trait in JS code
pub js_name: String,
/// All the methods of this trait to export
pub methods: Vec<TraitMethod>,
/// The doc comments on this trait, if provided
pub comments: Vec<String>,
/// Whether to generate a typescript definition for this trait
pub generate_typescript: bool,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any point to have this field? traits/interfaces only exist in TS

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry i never answered this. You can use traits without typescript definitions. The proposed way of doing this was making the interface contain Symbols for each method. So when you want to run the function of a class implementing such trait you do

struct[Trait.someMethodName]()

This is still the same in Typescript, because when you add a method of the same name on a class on Typescript, you override the method definition of the interface. Which is not normal behavior on rust

/// The supertraits of this trait, if provided
pub supertraits: Vec<Ident>,
}

/// The method signature of a trait
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
#[derive(Clone)]
pub struct TraitMethod {
/// Comments extracted from the rust source.
pub comments: Vec<String>,
/// The rust method
pub function: Function,
/// The kind (static, named, regular)
pub method_kind: MethodKind,
/// The type of `self` (either `self`, `&self`, or `&mut self`)
pub method_self: Option<MethodSelf>,
/// The trait name, in Rust, this is attached to
pub trait_name: Ident,
/// The name of the method on the rust side.
pub rust_name: Ident,
/// The name of the method in JS code
pub js_name: String,
}

/// Unused, the type of an argument to / return from a function
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TypeKind {
Expand Down
35 changes: 35 additions & 0 deletions crates/backend/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ fn shared_program<'a>(
.iter()
.map(|js| intern.intern_str(js))
.collect(),
traits: prog
.traits
.iter()
.map(|a| shared_trait(a, intern))
.collect::<Result<Vec<_>, _>>()?,
unique_crate_identifier: intern.intern_str(&intern.unique_crate_identifier()),
package_json: if intern.has_package_json.get() {
Some(intern.intern_str(intern.root.join("package.json").to_str().unwrap()))
Expand Down Expand Up @@ -328,6 +333,36 @@ fn shared_struct_field<'a>(s: &'a ast::StructField, _intern: &'a Interner) -> St
}
}

fn shared_trait<'a>(t: &'a ast::Trait, intern: &'a Interner) -> Result<Trait<'a>, Diagnostic> {
Ok(Trait {
name: &t.js_name,
methods: t
.methods
.iter()
.map(|t| shared_trait_method(t, intern))
.collect::<Result<Vec<_>, _>>()?,
comments: t.comments.iter().map(|t| &**t).collect(),
generate_typescript: t.generate_typescript,
})
}

fn shared_trait_method<'a>(
method: &'a ast::TraitMethod,
intern: &'a Interner,
) -> Result<TraitMethod<'a>, Diagnostic> {
let consumed = match method.method_self {
Some(ast::MethodSelf::ByValue) => true,
_ => false,
};
let method_kind = from_ast_method_kind(&method.function, intern, &method.method_kind)?;
Ok(TraitMethod {
comments: method.comments.iter().map(|s| &**s).collect(),
consumed,
function: shared_function(&method.function, intern),
method_kind,
})
}

trait Encode {
fn encode(&self, dst: &mut Encoder);
}
Expand Down
76 changes: 74 additions & 2 deletions crates/cli-support/src/js/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::descriptor::VectorKind;
use crate::descriptor::{VectorKind, Function};
use crate::intrinsic::Intrinsic;
use crate::wit::{Adapter, AdapterId, AdapterJsImportKind, AuxValue};
use crate::wit::{AdapterKind, Instruction, InstructionData};
use crate::wit::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct};
use crate::wit::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct, AuxTrait};
use crate::wit::{JsImport, JsImportName, NonstandardWitSection, WasmBindgenAux};
use crate::{reset_indentation, Bindgen, EncodeInto, OutputMode, PLACEHOLDER_MODULE};
use anyhow::{anyhow, bail, Context as _, Error};
Expand Down Expand Up @@ -2265,6 +2265,10 @@ impl<'a> Context<'a> {
self.generate_enum(e)?;
}

for t in self.aux.traits.iter() {
self.generate_trait(t)?;
}

for s in self.aux.structs.iter() {
self.generate_struct(s)?;
}
Expand Down Expand Up @@ -3327,6 +3331,74 @@ impl<'a> Context<'a> {
Ok(())
}

fn generate_trait(&mut self, trait_: &AuxTrait) -> Result<(), Error> {
let docs = format_doc_comments(&trait_.comments, None);
let mut symbols = String::new();
let mut interface = String::new();

if trait_.generate_typescript {
self.typescript.push_str(&docs);
self.typescript
.push_str(&format!("interface __wbindgen_{}Trait {{", trait_.name));
interface.push_str(&docs);
interface.push_str(&format!("export interface {} {{", trait_.name));
}
for method in trait_.methods.iter() {
let (symbol_docs, _method_docs) = if method.comments.is_empty() {
(String::new(), String::new())
} else {
// How do I generate JSDoc for this
(
format_doc_comments(&method.comments, None),
format_doc_comments(&method.comments, None),
)
};
if !symbol_docs.is_empty() {
symbols.push_str("\n");
symbols.push_str(&symbol_docs);
}
let name = match &method.kind {
AuxExportKind::Function(_) | AuxExportKind::Constructor(_) => {
bail!("this shouldn't be possible")
}
AuxExportKind::Getter { field: name, .. }
| AuxExportKind::Setter { field: name, .. }
| AuxExportKind::StaticFunction { name, .. }
| AuxExportKind::Method { name, .. } => name.clone(),
};
symbols.push_str(&format!("{name}:Symbol(\"{}.{name}\"),", trait_.name, name = name));
if trait_.generate_typescript {
self.typescript.push_str("\n");
self.typescript.push_str(&format_doc_comments(
&format!(" Symbol for the {} method", name),
None,
));
self.typescript
.push_str(&format!(" readonly {}: unique symbol;", name));
interface.push_str("\n");

//How do I generate ts sig for this?
//interface.push_str()
}
}
if trait_.generate_typescript {
self.typescript.push_str("\n}\n");
self.typescript.push_str(&format!(
"export const {name}: __wbindgen_{name}Trait;\n",
name = trait_.name
));
self.typescript.push_str(&interface);
self.typescript.push_str("\n}\n");
}
self.export(
&trait_.name,
&format!("Object.freeze({{ {} }})", symbols),
Some(&docs),
)?;

Ok(())
}

fn generate_struct(&mut self, struct_: &AuxStruct) -> Result<(), Error> {
let class = require_class(&mut self.exported_classes, &struct_.name);
class.comments = format_doc_comments(&struct_.comments, None);
Expand Down
56 changes: 56 additions & 0 deletions crates/cli-support/src/wit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ impl<'a> Context<'a> {
enums,
imports,
structs,
traits,
typescript_custom_sections,
local_modules,
inline_js,
Expand Down Expand Up @@ -385,6 +386,9 @@ impl<'a> Context<'a> {
for struct_ in structs {
self.struct_(struct_)?;
}
for trait_ in traits {
self.trait_(trait_)?;
}
for section in typescript_custom_sections {
self.aux.extra_typescript.push_str(section);
self.aux.extra_typescript.push_str("\n\n");
Expand Down Expand Up @@ -876,6 +880,58 @@ impl<'a> Context<'a> {
Ok(())
}

fn trait_(&mut self, trait_: decode::Trait<'_>) -> Result<(), Error> {
let mut methods = vec![];
for export in trait_.methods {
let wasm_name = struct_function_export_name(trait_.name, export.function.name);

let interface = trait_.name;
let kind = match export.method_kind {
decode::MethodKind::Constructor => bail!("traits can't have constructors"),
decode::MethodKind::Operation(op) => match op.kind {
decode::OperationKind::Getter(f) => AuxExportKind::Getter {
class: interface.to_string(),
field: f.to_string(),
consumed: export.consumed,
},
decode::OperationKind::Setter(f) => AuxExportKind::Setter {
class: interface.to_string(),
field: f.to_string(),
consumed: export.consumed,
},
_ if op.is_static => AuxExportKind::StaticFunction {
class: interface.to_string(),
name: export.function.name.to_string(),
},
_ => AuxExportKind::Method {
class: interface.to_string(),
name: export.function.name.to_string(),
consumed: export.consumed,
},
},
};

methods.push(AuxTraitMethod {
debug_name: wasm_name,
comments: concatenate_comments(&export.comments),
arg_names: Some(export.function.arg_names),
asyncness: export.function.asyncness,
kind,
generate_typescript: export.function.generate_typescript,
});
}

let aux = AuxTrait {
name: trait_.name.to_string(),
comments: concatenate_comments(&trait_.comments),
methods,
generate_typescript: trait_.generate_typescript,
};
self.aux.traits.push(aux);

Ok(())
}

fn determine_import(&self, import: &decode::Import<'_>, item: &str) -> Result<JsImport, Error> {
let is_local_snippet = match import.module {
decode::ImportModule::Named(s) => self.aux.local_modules.contains_key(s),
Expand Down
34 changes: 34 additions & 0 deletions crates/cli-support/src/wit/nonstandard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub struct WasmBindgenAux {
/// exported structs from Rust and their fields they've got exported.
pub structs: Vec<AuxStruct>,

/// Auxiliary information to go into JS/TypeScript bindings describing the
/// exported traits from Rust and their method info
pub traits: Vec<AuxTrait>,

/// Information about various internal functions used to manage the `externref`
/// table, later used to process JS bindings.
pub externref_table: Option<walrus::TableId>,
Expand Down Expand Up @@ -165,6 +169,36 @@ pub struct AuxStruct {
pub generate_typescript: bool,
}

#[derive(Debug)]
pub struct AuxTrait {
/// The name of this trait
pub name: String,
/// The copied Rust comments to forward to JS
pub comments: String,
/// The method signatures the trait defines
pub methods: Vec<AuxTraitMethod>,
/// Whether typescript bindings should be generated for this trait.
pub generate_typescript: bool,
}

#[derive(Debug)]
pub struct AuxTraitMethod {
/// When generating errors about this method, a helpful name to remember it
/// by.
pub debug_name: String,
/// Comments parsed in Rust and forwarded here to show up in JS bindings.
pub comments: String,
/// Argument names in Rust forwarded here to configure the names that show
/// up in TypeScript bindings.
pub arg_names: Option<Vec<String>>,
/// Whether this is an async function, to configure the TypeScript return value.
pub asyncness: bool,
/// What kind of function this is and where it shows up
pub kind: AuxExportKind,
/// Whether typescript bindings should be generated for this export.
pub generate_typescript: bool,
}

/// All possible types of imports that can be imported by a wasm module.
///
/// This `enum` is intended to map out what an imported value is. For example
Expand Down
9 changes: 9 additions & 0 deletions crates/cli-support/src/wit/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub fn add(module: &mut Module) -> Result<(), Error> {
imports_with_assert_no_shim: _, // not relevant for this purpose
enums,
structs,
traits,

// irrelevant ids used to track various internal intrinsics and such
externref_table: _,
Expand Down Expand Up @@ -200,6 +201,14 @@ pub fn add(module: &mut Module) -> Result<(), Error> {
);
}

if let Some(trait_) = traits.iter().next() {
bail!(
"generating a bindings section is currently incompatible with \
exporting a `trait` from the wasm file, cannot export `{}`",
trait_.name,
);
}

module.customs.add(section);
Ok(())
}
Expand Down
Loading