Skip to content

Commit 664c3f8

Browse files
authored
Update support for weak references (#2248)
* Update support for weak referenes This commit updates the `WASM_BINDGEN_WEAKREF` feature support to the latest version of the weak references proposal in JS. This also updates the `Closure` type to use finalization registries to ensure closures are deallocated with the JS gc. This means that `Closure::forget` will not actually leak memory in a weakref-using application. * Add a flag for weak references
1 parent 60f3b1d commit 664c3f8

File tree

11 files changed

+144
-49
lines changed

11 files changed

+144
-49
lines changed

azure-pipelines.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ jobs:
4848
# displayName: "(anyref) Crate test suite (no debug)"
4949
# - script: cargo test --target wasm32-unknown-unknown --features serde-serialize --test wasm
5050
# displayName: "(anyref) Crate test suite (with serde)"
51+
- script: |
52+
set -e
53+
echo "##vso[task.setvariable variable=NODE_ARGS]--harmony-weak-refs"
54+
echo "##vso[task.setvariable variable=WASM_BINDGEN_WEAKREF]1"
55+
displayName: "Configure anyref passes"
56+
- script: cargo test --target wasm32-unknown-unknown --test wasm
57+
displayName: "(weakref) Crate test suite"
58+
- script: WASM_BINDGEN_NO_DEBUG=1 cargo test --target wasm32-unknown-unknown --test wasm
59+
displayName: "(weakref) Crate test suite (no debug)"
60+
- script: cargo test --target wasm32-unknown-unknown --features serde-serialize --test wasm
61+
displayName: "(weakref) Crate test suite (with serde)"
5162

5263
- job: test_wasm_bindgen_windows
5364
displayName: "Run wasm-bindgen crate tests (Windows)"

crates/cli-support/src/intrinsic.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ intrinsics! {
114114
#[symbol = "__wbindgen_cb_drop"]
115115
#[signature = fn(Externref) -> Boolean]
116116
CallbackDrop,
117-
#[symbol = "__wbindgen_cb_forget"]
118-
#[signature = fn(Externref) -> Unit]
119-
CallbackForget,
120117
#[symbol = "__wbindgen_number_new"]
121118
#[signature = fn(F64) -> Externref]
122119
NumberNew,

crates/cli-support/src/js/mod.rs

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ impl<'a> Context<'a> {
781781
",
782782
name,
783783
if self.config.weak_refs {
784-
format!("{}FinalizationGroup.register(obj, obj.ptr, obj.ptr);", name)
784+
format!("{}Finalization.register(obj, obj.ptr, obj);", name)
785785
} else {
786786
String::new()
787787
},
@@ -790,13 +790,7 @@ impl<'a> Context<'a> {
790790

791791
if self.config.weak_refs {
792792
self.global(&format!(
793-
"
794-
const {}FinalizationGroup = new FinalizationGroup((items) => {{
795-
for (const ptr of items) {{
796-
wasm.{}(ptr);
797-
}}
798-
}});
799-
",
793+
"const {}Finalization = new FinalizationRegistry(ptr => wasm.{}(ptr));",
800794
name,
801795
wasm_bindgen_shared::free_function(&name),
802796
));
@@ -860,7 +854,7 @@ impl<'a> Context<'a> {
860854
}}
861855
",
862856
if self.config.weak_refs {
863-
format!("{}FinalizationGroup.unregister(ptr);", name)
857+
format!("{}Finalization.unregister(this);", name)
864858
} else {
865859
String::new()
866860
},
@@ -1938,6 +1932,16 @@ impl<'a> Context<'a> {
19381932

19391933
let table = self.export_function_table()?;
19401934

1935+
let (register, unregister) = if self.config.weak_refs {
1936+
self.expose_closure_finalization()?;
1937+
(
1938+
"CLOSURE_DTORS.register(real, state, state);",
1939+
"CLOSURE_DTORS.unregister(state)",
1940+
)
1941+
} else {
1942+
("", "")
1943+
};
1944+
19411945
// For mutable closures they can't be invoked recursively.
19421946
// To handle that we swap out the `this.a` pointer with zero
19431947
// while we invoke it. If we finish and the closure wasn't
@@ -1946,7 +1950,7 @@ impl<'a> Context<'a> {
19461950
self.global(&format!(
19471951
"
19481952
function makeMutClosure(arg0, arg1, dtor, f) {{
1949-
const state = {{ a: arg0, b: arg1, cnt: 1 }};
1953+
const state = {{ a: arg0, b: arg1, cnt: 1, dtor }};
19501954
const real = (...args) => {{
19511955
// First up with a closure we increment the internal reference
19521956
// count. This ensures that the Rust closure environment won't
@@ -1957,15 +1961,22 @@ impl<'a> Context<'a> {
19571961
try {{
19581962
return f(a, state.b, ...args);
19591963
}} finally {{
1960-
if (--state.cnt === 0) wasm.{}.get(dtor)(a, state.b);
1961-
else state.a = a;
1964+
if (--state.cnt === 0) {{
1965+
wasm.{table}.get(state.dtor)(a, state.b);
1966+
{unregister}
1967+
}} else {{
1968+
state.a = a;
1969+
}}
19621970
}}
19631971
}};
19641972
real.original = state;
1973+
{register}
19651974
return real;
19661975
}}
19671976
",
1968-
table
1977+
table = table,
1978+
register = register,
1979+
unregister = unregister,
19691980
));
19701981

19711982
Ok(())
@@ -1978,6 +1989,16 @@ impl<'a> Context<'a> {
19781989

19791990
let table = self.export_function_table()?;
19801991

1992+
let (register, unregister) = if self.config.weak_refs {
1993+
self.expose_closure_finalization()?;
1994+
(
1995+
"CLOSURE_DTORS.register(real, state, state);",
1996+
"CLOSURE_DTORS.unregister(state)",
1997+
)
1998+
} else {
1999+
("", "")
2000+
};
2001+
19812002
// For shared closures they can be invoked recursively so we
19822003
// just immediately pass through `this.a`. If we end up
19832004
// executing the destructor, however, we clear out the
@@ -1986,7 +2007,7 @@ impl<'a> Context<'a> {
19862007
self.global(&format!(
19872008
"
19882009
function makeClosure(arg0, arg1, dtor, f) {{
1989-
const state = {{ a: arg0, b: arg1, cnt: 1 }};
2010+
const state = {{ a: arg0, b: arg1, cnt: 1, dtor }};
19902011
const real = (...args) => {{
19912012
// First up with a closure we increment the internal reference
19922013
// count. This ensures that the Rust closure environment won't
@@ -1996,21 +2017,42 @@ impl<'a> Context<'a> {
19962017
return f(state.a, state.b, ...args);
19972018
}} finally {{
19982019
if (--state.cnt === 0) {{
1999-
wasm.{}.get(dtor)(state.a, state.b);
2020+
wasm.{table}.get(state.dtor)(state.a, state.b);
20002021
state.a = 0;
2022+
{unregister}
20012023
}}
20022024
}}
20032025
}};
20042026
real.original = state;
2027+
{register}
20052028
return real;
20062029
}}
20072030
",
2008-
table
2031+
table = table,
2032+
register = register,
2033+
unregister = unregister,
20092034
));
20102035

20112036
Ok(())
20122037
}
20132038

2039+
fn expose_closure_finalization(&mut self) -> Result<(), Error> {
2040+
if !self.should_write_global("closure_finalization") {
2041+
return Ok(());
2042+
}
2043+
assert!(self.config.weak_refs);
2044+
let table = self.export_function_table()?;
2045+
self.global(&format!(
2046+
"
2047+
const CLOSURE_DTORS = new FinalizationRegistry(state => {{
2048+
wasm.{}.get(state.dtor)(state.a, state.b)
2049+
}});
2050+
",
2051+
table
2052+
));
2053+
2054+
Ok(())
2055+
}
20142056
fn global(&mut self, s: &str) {
20152057
let s = s.trim();
20162058

@@ -2893,11 +2935,6 @@ impl<'a> Context<'a> {
28932935
"false".to_string()
28942936
}
28952937

2896-
Intrinsic::CallbackForget => {
2897-
assert_eq!(args.len(), 1);
2898-
args[0].clone()
2899-
}
2900-
29012938
Intrinsic::NumberNew => {
29022939
assert_eq!(args.len(), 1);
29032940
args[0].clone()

crates/cli-support/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ impl Bindgen {
128128
self
129129
}
130130

131+
pub fn weak_refs(&mut self, enable: bool) -> &mut Bindgen {
132+
self.weak_refs = enable;
133+
self
134+
}
135+
131136
/// Explicitly specify the already parsed input module.
132137
pub fn input_module(&mut self, name: &str, module: Module) -> &mut Bindgen {
133138
let name = name.to_string();

crates/cli/src/bin/wasm-bindgen.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Options:
3939
--nodejs Deprecated, use `--target nodejs`
4040
--web Deprecated, use `--target web`
4141
--no-modules Deprecated, use `--target no-modules`
42+
--weak-refs Enable usage of the JS weak references proposal
4243
-V --version Print the version number of wasm-bindgen
4344
";
4445

@@ -59,6 +60,7 @@ struct Args {
5960
flag_no_modules_global: Option<String>,
6061
flag_remove_name_section: bool,
6162
flag_remove_producers_section: bool,
63+
flag_weak_refs: Option<bool>,
6264
flag_keep_debug: bool,
6365
flag_encode_into: Option<String>,
6466
flag_target: Option<String>,
@@ -114,6 +116,9 @@ fn rmain(args: &Args) -> Result<(), Error> {
114116
.remove_producers_section(args.flag_remove_producers_section)
115117
.typescript(typescript)
116118
.omit_imports(args.flag_omit_imports);
119+
if flags.flag_weak_refs {
120+
b.weak_refs(true);
121+
}
117122
if let Some(ref name) = args.flag_no_modules_global {
118123
b.no_modules_global(name)?;
119124
}

crates/js-sys/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ extern "C" {
778778
/// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)
779779
#[wasm_bindgen(constructor)]
780780
#[deprecated(note = "recommended to use `Boolean::from` instead")]
781+
#[allow(deprecated)]
781782
pub fn new(value: &JsValue) -> Boolean;
782783

783784
/// The `valueOf()` method returns the primitive value of a `Boolean` object.
@@ -1843,6 +1844,7 @@ extern "C" {
18431844
/// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)
18441845
#[wasm_bindgen(constructor)]
18451846
#[deprecated(note = "recommended to use `Number::from` instead")]
1847+
#[allow(deprecated)]
18461848
pub fn new(value: &JsValue) -> Number;
18471849

18481850
/// The `Number.parseInt()` method parses a string argument and returns an

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
- [Optimizing for Size](./reference/optimize-size.md)
4242
- [Supported Rust Targets](./reference/rust-targets.md)
4343
- [Supported Browsers](./reference/browser-support.md)
44+
- [Support for Weak References](./reference/weak-references.md)
4445
- [Supported Types](./reference/types.md)
4546
- [Imported JavaScript Types](./reference/types/imported-js-types.md)
4647
- [Exported Rust Types](./reference/types/exported-rust-types.md)

guide/src/reference/cli.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,23 @@ always be listed via `wasm-bindgen --help`.
1313

1414
The recommend way to install the `wasm-bindgen` command line tool is with the
1515
`wasm-pack` installer described
16-
[here](https://rustwasm.github.io/wasm-pack/installer/). After installing
16+
[here](https://rustwasm.github.io/wasm-pack/installer/). After installing
1717
`wasm-pack`, you are ready to build project invoking `wasm-pack build`.
1818
This command installs apropriate version of the `wasm-bindgen` command-line
1919
tool. The version of `wasm-bindgen` installed by `wasm-pack` is not available
2020
to be used directly via command line.
2121

22-
It is not recommended to install `wasm-bindgen-cli` as its version must match
23-
_exactly_ the version of `wasm-bindgen` that is specified in the project's
24-
cargo.lock file. Using `wasm-pack` for building simplifies the build process
25-
as `wasm-pack` ensures that the proper version of `wasm-bindgen` command-line
26-
tool is used. That means that `wasm-pack` may install many different versions
27-
of `wasm-bindgen`, but during the build `wasm-pack` will always make sure to
22+
It is not recommended to install `wasm-bindgen-cli` as its version must match
23+
_exactly_ the version of `wasm-bindgen` that is specified in the project's
24+
cargo.lock file. Using `wasm-pack` for building simplifies the build process
25+
as `wasm-pack` ensures that the proper version of `wasm-bindgen` command-line
26+
tool is used. That means that `wasm-pack` may install many different versions
27+
of `wasm-bindgen`, but during the build `wasm-pack` will always make sure to
2828
use the correct one.
2929

30-
Note: if, for any reason, you decide to use wasm-bindgen directly (this is
31-
not recommended!) you will have to manually take care of using exactly the
32-
same version of wasm-bindgen command-line tool (wasm-bindgen-cli) that
30+
Note: if, for any reason, you decide to use wasm-bindgen directly (this is
31+
not recommended!) you will have to manually take care of using exactly the
32+
same version of wasm-bindgen command-line tool (wasm-bindgen-cli) that
3333
matches the version of wasm-bingden in cargo.lock.
3434

3535

@@ -101,3 +101,12 @@ sections.
101101
When generating bundler-compatible code (see the section on [deployment]) this
102102
indicates that the bundled code is always intended to go into a browser so a few
103103
checks for Node.js can be elided.
104+
105+
### `--weak-refs`
106+
107+
Enables usage of the [TC39 Weak References
108+
proposal](https://github.com/tc39/proposal-weakrefs), ensuring that all Rust
109+
memory is eventually deallocated regardless of whether you're calling `free` or
110+
not. This is off-by-default while we're waiting for support to percolate into
111+
all major browsers. For more information see the [documentation about weak
112+
references](./weak-references.md).
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Support for Weak References
2+
3+
By default wasm-bindgen does not uses the [TC39 weak references
4+
proposal](https://github.com/tc39/proposal-weakrefs). This proposal just
5+
advanced to stage 4 at the time of this writing, but it will still stake some
6+
time for support to percolate into all the major browsers.
7+
8+
Without weak references your JS integration may be susceptible to memory leaks
9+
in Rust, for example:
10+
11+
* You could forget to call `.free()` on a JS object, leaving the Rust memory
12+
allocated.
13+
* Rust closures converted to JS values (the `Closure` type) may not be executed
14+
and cleaned up.
15+
* Rust closures have a `Closure::forget` method which explicitly doesn't free
16+
the underlying memory.
17+
18+
These issues are all solved with the weak references proposal in JS. The
19+
`--weak-refs` flag to the `wasm-bindgen` CLI will enable usage of
20+
`FinalizationRegistry` to ensure that all memory is cleaned up, regardless of
21+
whether it's explicitly deallocated or not. Note that explicit deallocation
22+
is always a possibility and supported, but if it's not called then memory will
23+
still be automatically deallocated with the `--weak-refs` flag.

src/closure.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -356,22 +356,28 @@ where
356356
}
357357
}
358358

359-
/// Leaks this `Closure` to ensure it remains valid for the duration of the
360-
/// entire program.
359+
/// Release memory management of this closure from Rust to the JS GC.
361360
///
362-
/// > **Note**: this function will leak memory. It should be used sparingly
363-
/// > to ensure the memory leak doesn't affect the program too much.
361+
/// When a `Closure` is dropped it will release the Rust memory and
362+
/// invalidate the associated JS closure, but this isn't always desired.
363+
/// Some callbacks are alive for the entire duration of the program or for a
364+
/// lifetime dynamically managed by the JS GC. This function can be used
365+
/// to drop this `Closure` while keeping the associated JS function still
366+
/// valid.
364367
///
365-
/// When a `Closure` is dropped it will invalidate the associated JS
366-
/// closure, but this isn't always desired. Some callbacks are alive for
367-
/// the entire duration of the program, so this can be used to conveniently
368-
/// leak this instance of `Closure` while performing as much internal
369-
/// cleanup as it can.
370-
pub fn forget(self) {
371-
unsafe {
372-
super::__wbindgen_cb_forget(self.js.idx);
373-
mem::forget(self);
374-
}
368+
/// By default this function will leak memory. This can be dangerous if this
369+
/// function is called many times in an application because the memory leak
370+
/// will overwhelm the page quickly and crash the wasm.
371+
///
372+
/// If the browser, however, supports weak references, then this function
373+
/// will not leak memory. Instead the Rust memory will be reclaimed when the
374+
/// JS closure is GC'd. Weak references are not enabled by default since
375+
/// they're still a proposal for the JS standard. They can be enabled with
376+
/// `WASM_BINDGEN_WEAKREF=1` when running `wasm-bindgen`, however.
377+
pub fn forget(self) -> JsValue {
378+
let idx = self.js.idx;
379+
mem::forget(self);
380+
JsValue::_new(idx)
375381
}
376382
}
377383

src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,6 @@ externs! {
515515
fn __wbindgen_rethrow(a: u32) -> !;
516516

517517
fn __wbindgen_cb_drop(idx: u32) -> u32;
518-
fn __wbindgen_cb_forget(idx: u32) -> ();
519518

520519
fn __wbindgen_describe(v: u32) -> ();
521520
fn __wbindgen_describe_closure(a: u32, b: u32, c: u32) -> u32;

0 commit comments

Comments
 (0)