From b79b6c43248673c9837d643239c2bff753fd0835 Mon Sep 17 00:00:00 2001 From: Caio Date: Thu, 11 Apr 2019 14:35:32 -0300 Subject: [PATCH] Getter/Setter for fields --- crates/backend/src/ast.rs | 27 +++---- crates/backend/src/encode.rs | 80 +++++++++++-------- crates/cli-support/src/js/mod.rs | 25 ++++-- crates/macro-support/src/parser.rs | 74 ++++++++++------- crates/shared/src/lib.rs | 5 +- .../on-js-imports/getter-and-setter.md | 53 ++++++++++++ tests/wasm/getter_and_setter_for_fields.js | 12 +++ tests/wasm/getter_and_setter_for_fields.rs | 46 +++++++++++ tests/wasm/main.rs | 1 + 9 files changed, 234 insertions(+), 89 deletions(-) create mode 100644 tests/wasm/getter_and_setter_for_fields.js create mode 100644 tests/wasm/getter_and_setter_for_fields.rs diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index e61f58781bc5..eb1f8471649a 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -34,19 +34,18 @@ pub struct Program { #[cfg_attr(feature = "extra-traits", derive(Debug))] #[derive(Clone)] pub struct Export { - /// The struct name, in Rust, this is attached to - pub rust_class: Option, + /// Comments extracted from the rust source. + pub comments: Vec, + /// The rust function + pub function: Function, /// The class name in JS this is attached to pub js_class: Option, + /// The kind (static, named, regular) + pub method_kind: MethodKind, /// The type of `self` (either `self`, `&self`, or `&mut self`) pub method_self: Option, - /// Whether or not this export is flagged as a constructor, returning an - /// instance of the `impl` type - pub is_constructor: bool, - /// The rust function - pub function: Function, - /// Comments extracted from the rust source. - pub comments: Vec, + /// The struct name, in Rust, this is attached to + pub rust_class: Option, /// The name of the rust function/method on the rust side. pub rust_name: Ident, /// Whether or not this function should be flagged as the wasm start @@ -341,28 +340,28 @@ impl ImportKind { } } -impl ImportFunction { +impl Function { /// If the rust object has a `fn xxx(&self) -> MyType` method, get the name for a getter in /// javascript (in this case `xxx`, so you can write `val = obj.xxx`) pub fn infer_getter_property(&self) -> &str { - &self.function.name + &self.name } /// If the rust object has a `fn set_xxx(&mut self, MyType)` style method, get the name /// for a setter in javascript (in this case `xxx`, so you can write `obj.xxx = val`) pub fn infer_setter_property(&self) -> Result { - let name = self.function.name.to_string(); + let name = self.name.to_string(); // if `#[wasm_bindgen(js_name = "...")]` is used then that explicitly // because it was hand-written anyway. - if self.function.renamed_via_js_name { + if self.renamed_via_js_name { return Ok(name); } // Otherwise we infer names based on the Rust function name. if !name.starts_with("set_") { bail_span!( - syn::token::Pub(self.function.name_span), + syn::token::Pub(self.name_span), "setters must start with `set_`, found: {}", name, ); diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs index b0cc9cb6a312..492b3e645a54 100644 --- a/crates/backend/src/encode.rs +++ b/crates/backend/src/encode.rs @@ -125,7 +125,7 @@ fn shared_program<'a>( .exports .iter() .map(|a| shared_export(a, intern)) - .collect(), + .collect::, _>>()?, structs: prog .structs .iter() @@ -172,21 +172,23 @@ fn shared_program<'a>( }) } -fn shared_export<'a>(export: &'a ast::Export, intern: &'a Interner) -> Export<'a> { - let (method, consumed) = match export.method_self { - Some(ast::MethodSelf::ByValue) => (true, true), - Some(_) => (true, false), - None => (false, false), +fn shared_export<'a>( + export: &'a ast::Export, + intern: &'a Interner, +) -> Result, Diagnostic> { + let consumed = match export.method_self { + Some(ast::MethodSelf::ByValue) => true, + _ => false, }; - Export { + let method_kind = from_ast_method_kind(&export.function, intern, &export.method_kind)?; + Ok(Export { class: export.js_class.as_ref().map(|s| &**s), - method, + comments: export.comments.iter().map(|s| &**s).collect(), consumed, - is_constructor: export.is_constructor, function: shared_function(&export.function, intern), - comments: export.comments.iter().map(|s| &**s).collect(), + method_kind, start: export.start, - } + }) } fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Function<'a> { @@ -260,30 +262,7 @@ fn shared_import_function<'a>( ) -> Result, Diagnostic> { let method = match &i.kind { ast::ImportFunctionKind::Method { class, kind, .. } => { - let kind = match kind { - ast::MethodKind::Constructor => MethodKind::Constructor, - ast::MethodKind::Operation(ast::Operation { is_static, kind }) => { - let is_static = *is_static; - let kind = match kind { - ast::OperationKind::Regular => OperationKind::Regular, - ast::OperationKind::Getter(g) => { - let g = g.as_ref().map(|g| intern.intern(g)); - OperationKind::Getter(g.unwrap_or_else(|| i.infer_getter_property())) - } - ast::OperationKind::Setter(s) => { - let s = s.as_ref().map(|s| intern.intern(s)); - OperationKind::Setter(match s { - Some(s) => s, - None => intern.intern_str(&i.infer_setter_property()?), - }) - } - ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter, - ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter, - ast::OperationKind::IndexingDeleter => OperationKind::IndexingDeleter, - }; - MethodKind::Operation(Operation { is_static, kind }) - } - }; + let kind = from_ast_method_kind(&i.function, intern, kind)?; Some(MethodData { class, kind }) } ast::ImportFunctionKind::Normal => None, @@ -507,3 +486,34 @@ macro_rules! encode_api { ); } wasm_bindgen_shared::shared_api!(encode_api); + +fn from_ast_method_kind<'a>( + function: &'a ast::Function, + intern: &'a Interner, + method_kind: &'a ast::MethodKind, +) -> Result, Diagnostic> { + Ok(match method_kind { + ast::MethodKind::Constructor => MethodKind::Constructor, + ast::MethodKind::Operation(ast::Operation { is_static, kind }) => { + let is_static = *is_static; + let kind = match kind { + ast::OperationKind::Getter(g) => { + let g = g.as_ref().map(|g| intern.intern(g)); + OperationKind::Getter(g.unwrap_or_else(|| function.infer_getter_property())) + } + ast::OperationKind::Regular => OperationKind::Regular, + ast::OperationKind::Setter(s) => { + let s = s.as_ref().map(|s| intern.intern(s)); + OperationKind::Setter(match s { + Some(s) => s, + None => intern.intern_str(&function.infer_setter_property()?), + }) + } + ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter, + ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter, + ast::OperationKind::IndexingDeleter => OperationKind::IndexingDeleter, + }; + MethodKind::Operation(Operation { is_static, kind }) + } + }) +} diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index d9f02ad6c032..fee8e0cf0a9a 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -791,6 +791,14 @@ impl<'a> Context<'a> { )) })?; + self.bind("__wbindgen_function_table", &|me| { + me.function_table_needed = true; + Ok(format!( + "function() {{ return {}; }}", + me.add_heap_object("wasm.__wbg_function_table") + )) + })?; + self.bind("__wbindgen_rethrow", &|me| { Ok(format!( "function(idx) {{ throw {}; }}", @@ -2641,14 +2649,15 @@ impl<'a, 'b> SubContext<'a, 'b> { Some(d) => d, }; - let function_name = if export.is_constructor { - "constructor" - } else { - &export.function.name + let (function_name, is_constructor, has_self_in_fn) = match &export.method_kind { + decode::MethodKind::Constructor => ("constructor", true, false), + decode::MethodKind::Operation(operation) => { + (export.function.name, false, !operation.is_static) + } }; let (js, ts, js_doc) = Js2Rust::new(function_name, self.cx) - .method(export.method, export.consumed) - .constructor(if export.is_constructor { + .method(has_self_in_fn, export.consumed) + .constructor(if is_constructor { Some(class_name) } else { None @@ -2673,12 +2682,12 @@ impl<'a, 'b> SubContext<'a, 'b> { class.typescript.push_str(" "); // Indentation - if export.is_constructor { + if is_constructor { if class.has_constructor { bail!("found duplicate constructor `{}`", export.function.name); } class.has_constructor = true; - } else if !export.method { + } else if !has_self_in_fn { class.contents.push_str("static "); class.typescript.push_str("static "); } diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 6ad5172e8a0b..af9bcc67f56c 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -373,22 +373,7 @@ impl<'a> ConvertToAst<(BindgenAttrs, &'a ast::ImportModule)> for syn::ForeignIte wasm.ret.clone() }; - let mut operation_kind = ast::OperationKind::Regular; - if let Some(g) = opts.getter() { - operation_kind = ast::OperationKind::Getter(g.clone()); - } - if let Some(s) = opts.setter() { - operation_kind = ast::OperationKind::Setter(s.clone()); - } - if opts.indexing_getter().is_some() { - operation_kind = ast::OperationKind::IndexingGetter; - } - if opts.indexing_setter().is_some() { - operation_kind = ast::OperationKind::IndexingSetter; - } - if opts.indexing_deleter().is_some() { - operation_kind = ast::OperationKind::IndexingDeleter; - } + let operation_kind = operation_kind(&opts); let kind = if opts.method().is_some() { let class = wasm.arguments.get(0).ok_or_else(|| { @@ -742,15 +727,21 @@ impl<'a> MacroParse<(Option, &'a mut TokenStream)> for syn::Item { bail_span!(&f.decl.inputs, "the start function cannot have arguments",); } } + let method_kind = ast::MethodKind::Operation(ast::Operation { + is_static: true, + kind: operation_kind(&opts), + }); + let rust_name = f.ident.clone(); + let start = opts.start().is_some(); program.exports.push(ast::Export { - rust_class: None, - js_class: None, - method_self: None, - is_constructor: false, comments, - rust_name: f.ident.clone(), - start: opts.start().is_some(), function: f.convert(opts)?, + js_class: None, + method_kind, + method_self: None, + rust_class: None, + rust_name, + start, }); } syn::Item::Struct(mut s) => { @@ -929,7 +920,6 @@ impl<'a, 'b> MacroParse<(&'a Ident, &'a str)> for &'b mut syn::ImplItemMethod { let opts = BindgenAttrs::find(&mut self.attrs)?; let comments = extract_doc_comments(&self.attrs); - let is_constructor = opts.constructor().is_some(); let (function, method_self) = function_from_decl( &self.sig.ident, &opts, @@ -939,16 +929,22 @@ impl<'a, 'b> MacroParse<(&'a Ident, &'a str)> for &'b mut syn::ImplItemMethod { true, Some(class), )?; - + let method_kind = if opts.constructor().is_some() { + ast::MethodKind::Constructor + } else { + let is_static = method_self.is_none(); + let kind = operation_kind(&opts); + ast::MethodKind::Operation(ast::Operation { is_static, kind }) + }; program.exports.push(ast::Export { - rust_class: Some(class.clone()), + comments, + function, js_class: Some(js_class.to_string()), + method_kind, method_self, - is_constructor, - function, - comments, - start: false, + rust_class: Some(class.clone()), rust_name: self.sig.ident.clone(), + start: false, }); opts.check_used()?; Ok(()) @@ -1281,3 +1277,23 @@ pub fn assert_all_attrs_checked() { assert_eq!(state.parsed.get(), state.checks.get()); }) } + +fn operation_kind(opts: &BindgenAttrs) -> ast::OperationKind { + let mut operation_kind = ast::OperationKind::Regular; + if let Some(g) = opts.getter() { + operation_kind = ast::OperationKind::Getter(g.clone()); + } + if let Some(s) = opts.setter() { + operation_kind = ast::OperationKind::Setter(s.clone()); + } + if opts.indexing_getter().is_some() { + operation_kind = ast::OperationKind::IndexingGetter; + } + if opts.indexing_setter().is_some() { + operation_kind = ast::OperationKind::IndexingSetter; + } + if opts.indexing_deleter().is_some() { + operation_kind = ast::OperationKind::IndexingDeleter; + } + operation_kind +} diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 6bbfefb6a56b..845b384f90d7 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -88,11 +88,10 @@ macro_rules! shared_api { struct Export<'a> { class: Option<&'a str>, - method: bool, + comments: Vec<&'a str>, consumed: bool, - is_constructor: bool, function: Function<'a>, - comments: Vec<&'a str>, + method_kind: MethodKind<'a>, start: bool, } diff --git a/guide/src/reference/attributes/on-js-imports/getter-and-setter.md b/guide/src/reference/attributes/on-js-imports/getter-and-setter.md index aadbf161aeb8..1f189b8b9807 100644 --- a/guide/src/reference/attributes/on-js-imports/getter-and-setter.md +++ b/guide/src/reference/attributes/on-js-imports/getter-and-setter.md @@ -59,6 +59,59 @@ extern "C" { } ``` +It is also possible to export `Rust` types with getters and setters into `JavaScript` types. For example, the following: + +```rust +#[wasm_bindgen] +#[derive(Default)] +pub struct Baz { + field1: Option, + field2: Option, +} + +#[wasm_bindgen] +impl Baz { + pub fn new() -> Self { + Self::default() + } + + #[wasm_bindgen(getter)] + pub fn field1(&self) -> Option { + self.field1 + } + + #[wasm_bindgen(setter)] + pub fn set_field1(&mut self, field1: f64) { + self.field1 = Some(field1); + } + + #[wasm_bindgen(method, getter)] + pub fn field2(&self) -> Option { + self.field2 + } + + #[wasm_bindgen(method, setter)] + pub fn set_field(&mut self, field2: i32) { + self.field2 = Some(field2); + } +} +``` + +Can be used in `JavaScript` like in this snippet: + + +```js +function stuff() { + let foo = Foo.new(); + + foo.field1 = 3.14; + console.log(foo.field1); + + foo.set_field2(123); + console.log(foo.get_field2()); +} +``` + Heads up! `getter` and `setter` functions are found on the constructor's prototype chain once at load time, cached, and then the cached accessor is invoked on each access. If you need to dynamically walk the prototype chain on diff --git a/tests/wasm/getter_and_setter_for_fields.js b/tests/wasm/getter_and_setter_for_fields.js new file mode 100644 index 000000000000..223fca9090f3 --- /dev/null +++ b/tests/wasm/getter_and_setter_for_fields.js @@ -0,0 +1,12 @@ +const wasm = require('wasm-bindgen-test.js'); +const assert = require('assert'); + +exports.js_getter_and_setter_for_fields = () => { + let baz = wasm.Baz.new(); + + baz.field1 = 3.14; + assert.equal(baz.field1, 3.14); + + baz.set_field2(123); + assert.equal(baz.field2(), 123); +}; diff --git a/tests/wasm/getter_and_setter_for_fields.rs b/tests/wasm/getter_and_setter_for_fields.rs new file mode 100644 index 000000000000..01a13219be61 --- /dev/null +++ b/tests/wasm/getter_and_setter_for_fields.rs @@ -0,0 +1,46 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_test::*; + +#[wasm_bindgen(module = "tests/wasm/getter_and_setter_for_fields.js")] +extern "C" { + fn js_getter_and_setter_for_fields(); +} + +#[wasm_bindgen_test] +fn rust_getter_and_setter_for_fields() { + js_getter_and_setter_for_fields(); +} + +#[wasm_bindgen] +#[derive(Default)] +pub struct Baz { + field1: Option, + field2: Option, +} + +#[wasm_bindgen] +impl Baz { + pub fn new() -> Self { + Self::default() + } + + #[wasm_bindgen(getter)] + pub fn field1(&self) -> Option { + self.field1 + } + + #[wasm_bindgen(setter)] + pub fn set_field1(&mut self, field1: f64) { + self.field1 = Some(field1); + } + + #[wasm_bindgen(method, getter)] + pub fn field2(&self) -> Option { + self.field2 + } + + #[wasm_bindgen(method, setter)] + pub fn set_field(&mut self, field2: i32) { + self.field2 = Some(field2); + } +} diff --git a/tests/wasm/main.rs b/tests/wasm/main.rs index fff182284168..9a5f6bc10314 100644 --- a/tests/wasm/main.rs +++ b/tests/wasm/main.rs @@ -23,6 +23,7 @@ pub mod duplicates; pub mod enums; #[path = "final.rs"] pub mod final_; +pub mod getter_and_setter_for_fields; pub mod import_class; pub mod imports; pub mod js_objects;