Skip to content

Commit c1df441

Browse files
authored
Merge pull request #127 from konstin/new
Support `new Foo(...)` to fix #115
2 parents 0e6325d + f63635f commit c1df441

File tree

5 files changed

+211
-86
lines changed

5 files changed

+211
-86
lines changed

DESIGN.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -536,31 +536,37 @@ available to JS through generated shims. If we take a look at the generated JS
536536
code for this we'll see:
537537

538538
```js
539-
import * as wasm from './foo_bg';
539+
import * as wasm from './js_hello_world_bg';
540540

541541
export class Foo {
542-
constructor(ptr) {
543-
this.ptr = ptr;
544-
}
542+
static __construct(ptr) {
543+
return new Foo(ptr);
544+
}
545545

546-
free() {
547-
const ptr = this.ptr;
548-
this.ptr = 0;
549-
wasm.__wbindgen_foo_free(ptr);
550-
}
546+
constructor(ptr) {
547+
this.ptr = ptr;
548+
}
551549

552-
static new(arg0) {
553-
const ret = wasm.foo_new(arg0);
554-
return new Foo(ret);
555-
}
550+
free() {
551+
const ptr = this.ptr;
552+
this.ptr = 0;
553+
wasm.__wbg_foo_free(ptr);
554+
}
556555

557-
get() {
558-
return wasm.foo_get(this.ptr);
559-
}
556+
static new(arg0) {
557+
const ret = wasm.foo_new(arg0);
558+
return Foo.__construct(ret)
559+
}
560560

561-
set(arg0) {
562-
wasm.foo_set(this.ptr, arg0);
563-
}
561+
get() {
562+
const ret = wasm.foo_get(this.ptr);
563+
return ret;
564+
}
565+
566+
set(arg0) {
567+
const ret = wasm.foo_set(this.ptr, arg0);
568+
return ret;
569+
}
564570
}
565571
```
566572

@@ -573,9 +579,7 @@ to JS:
573579
* Manual memory management is exposed in JS as well. The `free` function is
574580
required to be invoked to deallocate resources on the Rust side of things.
575581

576-
It's intended that `new Foo()` is never used in JS. When `wasm-bindgen` is run
577-
with `--debug` it'll actually emit assertions to this effect to ensure that
578-
instances of `Foo` are only constructed with the functions like `Foo.new` in JS.
582+
To be able to use `new Foo()`, you'd need to annotate `new` as `#[wasm_bindgen(constructor)]`.
579583

580584
One important aspect to note here, though, is that once `free` is called the JS
581585
object is "neutered" in that its internal pointer is nulled out. This means that

crates/backend/src/ast.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct Export {
1414
pub class: Option<syn::Ident>,
1515
pub method: bool,
1616
pub mutable: bool,
17+
pub constructor: Option<String>,
1718
pub function: Function,
1819
}
1920

@@ -116,6 +117,7 @@ impl Program {
116117
class: None,
117118
method: false,
118119
mutable: false,
120+
constructor: None,
119121
function: Function::from(f, opts),
120122
});
121123
}
@@ -126,8 +128,8 @@ impl Program {
126128
}
127129
syn::Item::Impl(mut i) => {
128130
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut i.attrs));
131+
self.push_impl(&mut i, opts);
129132
i.to_tokens(tokens);
130-
self.push_impl(i, opts);
131133
}
132134
syn::Item::ForeignMod(mut f) => {
133135
let opts = opts.unwrap_or_else(|| BindgenAttrs::find(&mut f.attrs));
@@ -145,7 +147,7 @@ impl Program {
145147
}
146148
}
147149

148-
pub fn push_impl(&mut self, item: syn::ItemImpl, _opts: BindgenAttrs) {
150+
pub fn push_impl(&mut self, item: &mut syn::ItemImpl, _opts: BindgenAttrs) {
149151
if item.defaultness.is_some() {
150152
panic!("default impls are not supported");
151153
}
@@ -168,16 +170,16 @@ impl Program {
168170
},
169171
_ => panic!("unsupported self type in impl"),
170172
};
171-
for item in item.items.into_iter() {
172-
self.push_impl_item(name, item);
173+
for mut item in item.items.iter_mut() {
174+
self.push_impl_item(name, &mut item);
173175
}
174176
}
175177

176-
fn push_impl_item(&mut self, class: syn::Ident, item: syn::ImplItem) {
177-
let mut method = match item {
178+
fn push_impl_item(&mut self, class: syn::Ident, item: &mut syn::ImplItem) {
179+
let method = match item {
178180
syn::ImplItem::Const(_) => panic!("const definitions aren't supported"),
179181
syn::ImplItem::Type(_) => panic!("type definitions in impls aren't supported"),
180-
syn::ImplItem::Method(m) => m,
182+
syn::ImplItem::Method(ref mut m) => m,
181183
syn::ImplItem::Macro(_) => panic!("macros in impls aren't supported"),
182184
syn::ImplItem::Verbatim(_) => panic!("unparsed impl item?"),
183185
};
@@ -196,19 +198,27 @@ impl Program {
196198
}
197199

198200
let opts = BindgenAttrs::find(&mut method.attrs);
201+
let is_constructor = opts.constructor();
202+
let constructor = if is_constructor {
203+
Some(method.sig.ident.to_string())
204+
} else {
205+
None
206+
};
199207

200208
let (function, mutable) = Function::from_decl(
201209
method.sig.ident,
202-
Box::new(method.sig.decl),
203-
method.attrs,
210+
Box::new(method.sig.decl.clone()),
211+
method.attrs.clone(),
204212
opts,
205-
method.vis,
213+
method.vis.clone(),
206214
true,
207215
);
216+
208217
self.exports.push(Export {
209218
class: Some(class),
210219
method: mutable.is_some(),
211220
mutable: mutable.unwrap_or(false),
221+
constructor,
212222
function,
213223
});
214224
}
@@ -536,6 +546,7 @@ impl Export {
536546
shared::Export {
537547
class: self.class.map(|s| s.as_ref().to_string()),
538548
method: self.method,
549+
constructor: self.constructor.clone(),
539550
function: self.function.shared(),
540551
}
541552
}
@@ -764,6 +775,7 @@ impl syn::synom::Synom for BindgenAttrs {
764775
));
765776
}
766777

778+
#[derive(PartialEq)]
767779
enum BindgenAttr {
768780
Catch,
769781
Constructor,

crates/cli-support/src/js.rs

Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct Context<'a> {
2929
pub struct ExportedClass {
3030
pub contents: String,
3131
pub typescript: String,
32+
pub constructor: Option<String>,
3233
}
3334

3435
pub struct SubContext<'a, 'b: 'a> {
@@ -316,42 +317,57 @@ impl<'a> Context<'a> {
316317
ts_dst.push_str("
317318
public ptr: number;
318319
");
319-
if self.config.debug {
320-
self.expose_check_token();
320+
321+
if self.config.debug || exports.constructor.is_some() {
322+
self.expose_constructor_token();
323+
321324
dst.push_str(&format!("
322-
constructor(ptr, sym) {{
323-
_checkToken(sym);
324-
this.ptr = ptr;
325+
static __construct(ptr) {{
326+
return new {}(new ConstructorToken(ptr));
325327
}}
326-
"));
327-
ts_dst.push_str("constructor(ptr: number, sym: Symbol);\n");
328-
329-
let new_name = shared::new_function(&class);
330-
if self.wasm_import_needed(&new_name) {
331-
self.expose_add_heap_object();
332-
self.export(&new_name, &format!("
333-
function(ptr) {{
334-
return addHeapObject(new {class}(ptr, token));
328+
329+
constructor(...args) {{
330+
if (args.length === 1 && args[0] instanceof ConstructorToken) {{
331+
this.ptr = args[0].ptr;
332+
return;
335333
}}
336-
", class = class));
334+
", class));
335+
336+
if let Some(constructor) = exports.constructor {
337+
ts_dst.push_str(&format!("constructor(...args: [any]);\n"));
338+
339+
dst.push_str(&format!("
340+
// This invocation of new will call this constructor with a ConstructorToken
341+
let instance = {class}.{constructor}(...args);
342+
this.ptr = instance.ptr;
343+
", class = class, constructor = constructor));
344+
} else {
345+
dst.push_str("throw new Error('you cannot invoke `new` directly without having a \
346+
method annotated a constructor');");
337347
}
348+
349+
dst.push_str("}");
338350
} else {
339351
dst.push_str(&format!("
352+
static __construct(ptr) {{
353+
return new {}(ptr);
354+
}}
355+
340356
constructor(ptr) {{
341357
this.ptr = ptr;
342358
}}
343-
"));
344-
ts_dst.push_str("constructor(ptr: number);\n");
345-
346-
let new_name = shared::new_function(&class);
347-
if self.wasm_import_needed(&new_name) {
348-
self.expose_add_heap_object();
349-
self.export(&new_name, &format!("
350-
function(ptr) {{
351-
return addHeapObject(new {class}(ptr));
352-
}}
353-
", class = class));
354-
}
359+
", class));
360+
}
361+
362+
let new_name = shared::new_function(&class);
363+
if self.wasm_import_needed(&new_name) {
364+
self.expose_add_heap_object();
365+
366+
self.export(&new_name, &format!("
367+
function(ptr) {{
368+
return addHeapObject({}.__construct(ptr));
369+
}}
370+
", class));
355371
}
356372

357373
dst.push_str(&format!("
@@ -589,19 +605,6 @@ impl<'a> Context<'a> {
589605
", get_obj));
590606
}
591607

592-
fn expose_check_token(&mut self) {
593-
if !self.exposed_globals.insert("check_token") {
594-
return;
595-
}
596-
self.globals.push_str(&format!("
597-
const token = Symbol('foo');
598-
function _checkToken(sym) {{
599-
if (token !== sym)
600-
throw new Error('cannot invoke `new` directly');
601-
}}
602-
"));
603-
}
604-
605608
fn expose_assert_num(&mut self) {
606609
if !self.exposed_globals.insert("assert_num") {
607610
return;
@@ -765,6 +768,20 @@ impl<'a> Context<'a> {
765768
"));
766769
}
767770

771+
fn expose_constructor_token(&mut self) {
772+
if !self.exposed_globals.insert("ConstructorToken") {
773+
return;
774+
}
775+
776+
self.globals.push_str("
777+
class ConstructorToken {
778+
constructor(ptr) {
779+
this.ptr = ptr;
780+
}
781+
}
782+
");
783+
}
784+
768785
fn expose_get_string_from_wasm(&mut self) {
769786
if !self.exposed_globals.insert("get_string_from_wasm") {
770787
return;
@@ -1236,11 +1253,8 @@ impl<'a> Context<'a> {
12361253
if let Some(name) = ty.rust_struct() {
12371254
dst_ts.push_str(": ");
12381255
dst_ts.push_str(name);
1239-
return if self.config.debug {
1240-
format!("return new {name}(ret, token);", name = name)
1241-
} else {
1242-
format!("return new {name}(ret);", name = name)
1243-
}
1256+
1257+
return format!("return {}.__construct(ret)",&name);
12441258
}
12451259

12461260
if ty.is_number() {
@@ -1324,8 +1338,8 @@ impl<'a, 'b> SubContext<'a, 'b> {
13241338
self.cx.typescript.push_str("\n");
13251339
}
13261340

1327-
pub fn generate_export_for_class(&mut self, class: &str, export: &shared::Export) {
1328-
let wasm_name = shared::struct_function_export_name(class, &export.function.name);
1341+
pub fn generate_export_for_class(&mut self, class_name: &str, export: &shared::Export) {
1342+
let wasm_name = shared::struct_function_export_name(class_name, &export.function.name);
13291343
let descriptor = self.cx.describe(&wasm_name);
13301344
let (js, ts) = self.generate_function(
13311345
"",
@@ -1334,12 +1348,26 @@ impl<'a, 'b> SubContext<'a, 'b> {
13341348
export.method,
13351349
&descriptor.unwrap_function(),
13361350
);
1337-
let class = self.cx.exported_classes.entry(class.to_string())
1351+
1352+
let class = self.cx.exported_classes.entry(class_name.to_string())
13381353
.or_insert(ExportedClass::default());
13391354
if !export.method {
13401355
class.contents.push_str("static ");
13411356
class.typescript.push_str("static ");
13421357
}
1358+
1359+
let constructors: Vec<String> = self.program.exports
1360+
.iter()
1361+
.filter(|x| x.class == Some(class_name.to_string()))
1362+
.filter_map(|x| x.constructor.clone())
1363+
.collect();
1364+
1365+
class.constructor = match constructors.len() {
1366+
0 => None,
1367+
1 => Some(constructors[0].clone()),
1368+
x @ _ => panic!("There must be only one constructor, not {}", x),
1369+
};
1370+
13431371
class.contents.push_str(&export.function.name);
13441372
class.contents.push_str(&js);
13451373
class.contents.push_str("\n");
@@ -1560,15 +1588,11 @@ impl<'a, 'b> SubContext<'a, 'b> {
15601588
continue
15611589
}
15621590

1563-
if let Some(s) = arg.rust_struct() {
1591+
if let Some(class) = arg.rust_struct() {
15641592
if arg.is_by_ref() {
15651593
panic!("cannot invoke JS functions with custom ref types yet")
15661594
}
1567-
let assign = if self.cx.config.debug {
1568-
format!("let c{0} = new {class}(arg{0}, token);", i, class = s)
1569-
} else {
1570-
format!("let c{0} = new {class}(arg{0});", i, class = s)
1571-
};
1595+
let assign = format!("let c{0} = {1}.__construct(arg{0});", i, class);
15721596
extra.push_str(&assign);
15731597
invoc_args.push(format!("c{}", i));
15741598
continue

crates/shared/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub struct ImportType {
5454
pub struct Export {
5555
pub class: Option<String>,
5656
pub method: bool,
57+
pub constructor: Option<String>,
5758
pub function: Function,
5859
}
5960

0 commit comments

Comments
 (0)