From 71082068357a896bbad636556c3c55acaf7d38ea Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 20 Apr 2018 10:56:10 -0700 Subject: [PATCH] Implement readonly struct fields Add support for `#[wasm_bindgen(readonly)]` which indicates that an exported struct field is readonly and attempting to set it in JS will throw an exception. Closes #151 --- DESIGN.md | 17 ++++++++++++++ crates/backend/src/ast.rs | 22 ++++++++++++++---- crates/backend/src/codegen.rs | 19 ++++++++++------ crates/cli-support/src/js/mod.rs | 25 ++++++++++++-------- crates/shared/src/lib.rs | 3 ++- tests/all/classes.rs | 39 ++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 22 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 82937a80..9fdfbb10 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1044,6 +1044,23 @@ possibilities! All of these functions will call `console.log` in Rust, but each identifier will have only one signature in Rust. +* `readonly` - when attached to a `pub` struct field this indicates that it's + readonly from JS and a setter will not be generated. + + ```rust + #[wasm_bindgen] + pub struct Foo { + pub first: u32, + #[wasm_bindgen(readonly)] + pub second: u32, + } + ``` + + Here the `first` field will be both readable and writable from JS, but the + `second` field will be a `readonly` field in JS where the setter isn't + implemented and attempting to set it will throw an exception. + + ## Rust Type conversions Previously we've been seeing mostly abridged versions of type conversions when diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index f5afd575..592c4afd 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -72,6 +72,7 @@ pub struct Struct { } pub struct StructField { + pub opts: BindgenAttrs, pub name: syn::Ident, pub struct_name: syn::Ident, pub ty: syn::Type, @@ -132,8 +133,8 @@ impl Program { } syn::Item::Struct(mut s) => { let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut s.attrs)); + self.structs.push(Struct::from(&mut s, opts)); s.to_tokens(tokens); - self.structs.push(Struct::from(s, opts)); } syn::Item::Impl(mut i) => { let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut i.attrs)); @@ -668,10 +669,10 @@ impl ImportType { } impl Struct { - fn from(s: syn::ItemStruct, _opts: BindgenAttrs) -> Struct { + fn from(s: &mut syn::ItemStruct, _opts: BindgenAttrs) -> Struct { let mut fields = Vec::new(); - if let syn::Fields::Named(names) = s.fields { - for field in names.named.iter() { + if let syn::Fields::Named(names) = &mut s.fields { + for field in names.named.iter_mut() { match field.vis { syn::Visibility::Public(..) => {} _ => continue, @@ -682,7 +683,9 @@ impl Struct { }; let getter = shared::struct_field_get(s.ident.as_ref(), name.as_ref()); let setter = shared::struct_field_set(s.ident.as_ref(), name.as_ref()); + let opts = BindgenAttrs::find(&mut field.attrs); fields.push(StructField { + opts, name, struct_name: s.ident, ty: field.ty.clone(), @@ -709,6 +712,7 @@ impl StructField { fn shared(&self) -> shared::StructField { shared::StructField { name: self.name.as_ref().to_string(), + readonly: self.opts.readonly(), } } } @@ -800,6 +804,13 @@ impl BindgenAttrs { }) } + pub fn readonly(&self) -> bool { + self.attrs.iter().any(|a| match *a { + BindgenAttr::Readonly => true, + _ => false, + }) + } + pub fn js_name(&self) -> Option { self.attrs .iter() @@ -836,6 +847,7 @@ enum BindgenAttr { Getter(Option), Setter(Option), Structural, + Readonly, JsName(syn::Ident), } @@ -869,6 +881,8 @@ impl syn::synom::Synom for BindgenAttr { | call!(term, "structural") => { |_| BindgenAttr::Structural } | + call!(term, "readonly") => { |_| BindgenAttr::Readonly } + | do_parse!( call!(term, "js_namespace") >> punct!(=) >> diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 3b0c99f6..a91c44df 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -247,6 +247,18 @@ impl ToTokens for ast::StructField { ) } + #[no_mangle] + pub extern fn #desc() { + use wasm_bindgen::describe::*; + <#ty as WasmDescribe>::describe(); + } + }).to_tokens(tokens); + + if self.opts.readonly() { + return + } + + (quote! { #[no_mangle] pub unsafe extern fn #setter( js: u32, @@ -263,13 +275,6 @@ impl ToTokens for ast::StructField { ); (*js).borrow_mut().#name = val; } - - #[no_mangle] - pub extern fn #desc() { - use wasm_bindgen::describe::*; - <#ty as WasmDescribe>::describe(); - - } }).to_tokens(tokens); } } diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs index 22477072..5bd784ce 100644 --- a/crates/cli-support/src/js/mod.rs +++ b/crates/cli-support/src/js/mod.rs @@ -32,14 +32,15 @@ pub struct Context<'a> { #[derive(Default)] pub struct ExportedClass { - pub contents: String, - pub typescript: String, - pub constructor: Option, - pub fields: Vec, + contents: String, + typescript: String, + constructor: Option, + fields: Vec, } -pub struct ClassField { - pub name: String, +struct ClassField { + name: String, + readonly: bool, } pub struct SubContext<'a, 'b: 'a> { @@ -416,7 +417,8 @@ impl<'a> Context<'a> { cx.method(true) .argument(&descriptor) .ret(&None); - ts_dst.push_str(&format!("{}: {}\n", + ts_dst.push_str(&format!("{}{}: {}\n", + if field.readonly { "readonly " } else { "" }, field.name, &cx.js_arguments[0].1)); cx.finish("", &format!("wasm.{}", wasm_setter)).0 @@ -430,9 +432,11 @@ impl<'a> Context<'a> { dst.push_str(&field.name); dst.push_str(&get); dst.push_str("\n"); - dst.push_str("set "); - dst.push_str(&field.name); - dst.push_str(&set); + if !field.readonly { + dst.push_str("set "); + dst.push_str(&field.name); + dst.push_str(&set); + } } dst.push_str(&format!(" @@ -1312,6 +1316,7 @@ impl<'a, 'b> SubContext<'a, 'b> { .extend(s.fields.iter().map(|s| { ClassField { name: s.name.clone(), + readonly: s.readonly, } })); } diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index 0bcf1731..adbe6ee0 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate serde_derive; -pub const SCHEMA_VERSION: &str = "3"; +pub const SCHEMA_VERSION: &str = "4"; #[derive(Deserialize)] pub struct ProgramOnlySchema { @@ -91,6 +91,7 @@ pub struct Struct { #[derive(Deserialize, Serialize)] pub struct StructField { pub name: String, + pub readonly: bool, } pub fn new_function(struct_name: &str) -> String { diff --git a/tests/all/classes.rs b/tests/all/classes.rs index 45b652b5..8188c5fb 100644 --- a/tests/all/classes.rs +++ b/tests/all/classes.rs @@ -558,3 +558,42 @@ fn using_self() { "#) .test(); } + +#[test] +fn readonly_fields() { + project() + .debug(false) + .file("src/lib.rs", r#" + #![feature(proc_macro, wasm_custom_section, wasm_import_module)] + + extern crate wasm_bindgen; + + use wasm_bindgen::prelude::*; + + #[wasm_bindgen] + #[derive(Default)] + pub struct Foo { + #[wasm_bindgen(readonly)] + pub a: u32, + } + + #[wasm_bindgen] + impl Foo { + pub fn new() -> Foo { + Foo::default() + } + } + "#) + .file("test.ts", r#" + import { Foo } from "./out"; + import * as assert from "assert"; + + export function test() { + const a = Foo.new(); + assert.strictEqual(a.a, 0); + assert.throws(() => (a as any).a = 3, /has only a getter/); + a.free(); + } + "#) + .test(); +}