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
This commit is contained in:
Alex Crichton
2018-04-20 10:56:10 -07:00
parent 3b4bf475be
commit 7108206835
6 changed files with 103 additions and 22 deletions

View File

@ -1044,6 +1044,23 @@ possibilities!
All of these functions will call `console.log` in Rust, but each identifier All of these functions will call `console.log` in Rust, but each identifier
will have only one signature in Rust. 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 ## Rust Type conversions
Previously we've been seeing mostly abridged versions of type conversions when Previously we've been seeing mostly abridged versions of type conversions when

View File

@ -72,6 +72,7 @@ pub struct Struct {
} }
pub struct StructField { pub struct StructField {
pub opts: BindgenAttrs,
pub name: syn::Ident, pub name: syn::Ident,
pub struct_name: syn::Ident, pub struct_name: syn::Ident,
pub ty: syn::Type, pub ty: syn::Type,
@ -132,8 +133,8 @@ impl Program {
} }
syn::Item::Struct(mut s) => { syn::Item::Struct(mut s) => {
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut s.attrs)); let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut s.attrs));
self.structs.push(Struct::from(&mut s, opts));
s.to_tokens(tokens); s.to_tokens(tokens);
self.structs.push(Struct::from(s, opts));
} }
syn::Item::Impl(mut i) => { syn::Item::Impl(mut i) => {
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut i.attrs)); let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut i.attrs));
@ -668,10 +669,10 @@ impl ImportType {
} }
impl Struct { impl Struct {
fn from(s: syn::ItemStruct, _opts: BindgenAttrs) -> Struct { fn from(s: &mut syn::ItemStruct, _opts: BindgenAttrs) -> Struct {
let mut fields = Vec::new(); let mut fields = Vec::new();
if let syn::Fields::Named(names) = s.fields { if let syn::Fields::Named(names) = &mut s.fields {
for field in names.named.iter() { for field in names.named.iter_mut() {
match field.vis { match field.vis {
syn::Visibility::Public(..) => {} syn::Visibility::Public(..) => {}
_ => continue, _ => continue,
@ -682,7 +683,9 @@ impl Struct {
}; };
let getter = shared::struct_field_get(s.ident.as_ref(), name.as_ref()); 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 setter = shared::struct_field_set(s.ident.as_ref(), name.as_ref());
let opts = BindgenAttrs::find(&mut field.attrs);
fields.push(StructField { fields.push(StructField {
opts,
name, name,
struct_name: s.ident, struct_name: s.ident,
ty: field.ty.clone(), ty: field.ty.clone(),
@ -709,6 +712,7 @@ impl StructField {
fn shared(&self) -> shared::StructField { fn shared(&self) -> shared::StructField {
shared::StructField { shared::StructField {
name: self.name.as_ref().to_string(), 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<syn::Ident> { pub fn js_name(&self) -> Option<syn::Ident> {
self.attrs self.attrs
.iter() .iter()
@ -836,6 +847,7 @@ enum BindgenAttr {
Getter(Option<syn::Ident>), Getter(Option<syn::Ident>),
Setter(Option<syn::Ident>), Setter(Option<syn::Ident>),
Structural, Structural,
Readonly,
JsName(syn::Ident), JsName(syn::Ident),
} }
@ -869,6 +881,8 @@ impl syn::synom::Synom for BindgenAttr {
| |
call!(term, "structural") => { |_| BindgenAttr::Structural } call!(term, "structural") => { |_| BindgenAttr::Structural }
| |
call!(term, "readonly") => { |_| BindgenAttr::Readonly }
|
do_parse!( do_parse!(
call!(term, "js_namespace") >> call!(term, "js_namespace") >>
punct!(=) >> punct!(=) >>

View File

@ -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] #[no_mangle]
pub unsafe extern fn #setter( pub unsafe extern fn #setter(
js: u32, js: u32,
@ -263,13 +275,6 @@ impl ToTokens for ast::StructField {
); );
(*js).borrow_mut().#name = val; (*js).borrow_mut().#name = val;
} }
#[no_mangle]
pub extern fn #desc() {
use wasm_bindgen::describe::*;
<#ty as WasmDescribe>::describe();
}
}).to_tokens(tokens); }).to_tokens(tokens);
} }
} }

View File

@ -32,14 +32,15 @@ pub struct Context<'a> {
#[derive(Default)] #[derive(Default)]
pub struct ExportedClass { pub struct ExportedClass {
pub contents: String, contents: String,
pub typescript: String, typescript: String,
pub constructor: Option<String>, constructor: Option<String>,
pub fields: Vec<ClassField>, fields: Vec<ClassField>,
} }
pub struct ClassField { struct ClassField {
pub name: String, name: String,
readonly: bool,
} }
pub struct SubContext<'a, 'b: 'a> { pub struct SubContext<'a, 'b: 'a> {
@ -416,7 +417,8 @@ impl<'a> Context<'a> {
cx.method(true) cx.method(true)
.argument(&descriptor) .argument(&descriptor)
.ret(&None); .ret(&None);
ts_dst.push_str(&format!("{}: {}\n", ts_dst.push_str(&format!("{}{}: {}\n",
if field.readonly { "readonly " } else { "" },
field.name, field.name,
&cx.js_arguments[0].1)); &cx.js_arguments[0].1));
cx.finish("", &format!("wasm.{}", wasm_setter)).0 cx.finish("", &format!("wasm.{}", wasm_setter)).0
@ -430,9 +432,11 @@ impl<'a> Context<'a> {
dst.push_str(&field.name); dst.push_str(&field.name);
dst.push_str(&get); dst.push_str(&get);
dst.push_str("\n"); dst.push_str("\n");
dst.push_str("set "); if !field.readonly {
dst.push_str(&field.name); dst.push_str("set ");
dst.push_str(&set); dst.push_str(&field.name);
dst.push_str(&set);
}
} }
dst.push_str(&format!(" dst.push_str(&format!("
@ -1312,6 +1316,7 @@ impl<'a, 'b> SubContext<'a, 'b> {
.extend(s.fields.iter().map(|s| { .extend(s.fields.iter().map(|s| {
ClassField { ClassField {
name: s.name.clone(), name: s.name.clone(),
readonly: s.readonly,
} }
})); }));
} }

View File

@ -1,7 +1,7 @@
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
pub const SCHEMA_VERSION: &str = "3"; pub const SCHEMA_VERSION: &str = "4";
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ProgramOnlySchema { pub struct ProgramOnlySchema {
@ -91,6 +91,7 @@ pub struct Struct {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct StructField { pub struct StructField {
pub name: String, pub name: String,
pub readonly: bool,
} }
pub fn new_function(struct_name: &str) -> String { pub fn new_function(struct_name: &str) -> String {

View File

@ -558,3 +558,42 @@ fn using_self() {
"#) "#)
.test(); .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();
}