Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Wasm weak references for automatic garbage collection #694

Merged
merged 14 commits into from
Mar 9, 2022
Merged
6 changes: 5 additions & 1 deletion .github/actions/publish/publish-wasm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ runs:
override: true
target: wasm32-unknown-unknown

# Download a pre-compiled wasm-bindgen binary.
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@v0.1.0
cycraig marked this conversation as resolved.
Show resolved Hide resolved

- name: Set up Node.js
uses: actions/setup-node@v2
with:
Expand All @@ -40,4 +44,4 @@ runs:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
# will publish 'latest' tag if no tag is passed
run: npm publish $(if [ ${{ inputs.tag }} != '' ]; then echo --tag ${{ inputs.tag }}; fi) --access public
working-directory: bindings/wasm
working-directory: bindings/wasm
20 changes: 12 additions & 8 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ jobs:
current-date: ${{ env.CURRENT_DATE }}

- name: Setup sccache
uses: './.github/actions/rust/sccache/setup-sccache'
with:
uses: './.github/actions/rust/sccache/setup-sccache'
with:
os: ${{matrix.os}}

- name: Build
uses: actions-rs/cargo@v1
with:
Expand All @@ -126,7 +126,7 @@ jobs:

- name: Stop sccache
uses: './.github/actions/rust/sccache/stop-sccache'
with:
with:
os: ${{matrix.os}}

build-and-test-libjose:
Expand Down Expand Up @@ -197,17 +197,21 @@ jobs:
with:
toolchain: stable
target: wasm32-unknown-unknown

- name: Setup sccache
uses: './.github/actions/rust/sccache/setup-sccache'
with:
uses: './.github/actions/rust/sccache/setup-sccache'
with:
os: ${{matrix.os}}

- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x

# Download a pre-compiled wasm-bindgen binary.
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@v0.1.0

- name: Install JS dependencies
run: npm install
working-directory: bindings/wasm
Expand All @@ -226,5 +230,5 @@ jobs:

- name: Stop sccache
uses: './.github/actions/rust/sccache/stop-sccache'
with:
with:
os: ${{matrix.os}}
4 changes: 4 additions & 0 deletions .github/workflows/clippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
override: true
components: clippy

# Download a pre-compiled wasm-bindgen binary.
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@v0.1.0

- name: core clippy check
uses: actions-rs/clippy-check@v1
with:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
override: true
components: llvm-tools-preview

# Download a pre-compiled wasm-bindgen binary.
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@v0.1.0

- name: Install cargo-binutils
uses: actions-rs/install@v0.1
with:
Expand Down
12 changes: 10 additions & 2 deletions bindings/wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ npm install @iota/identity-wasm@dev

## Build

Alternatively, you can build the bindings if you have Rust installed. If not, refer to [rustup.rs](https://rustup.rs) for the installation. Then install the necessary dependencies using:
Alternatively, you can build the bindings if you have Rust installed. If not, refer to [rustup.rs](https://rustup.rs) for the installation.

Install [`wasm-bindgen-cli`](https://github.com/rustwasm/wasm-bindgen). A manual installation is required because we use the [Weak References](https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html) feature, which [`wasm-pack` does not expose](https://github.com/rustwasm/wasm-pack/issues/930).

```bash
cargo install --force wasm-bindgen-cli
```

Then, install the necessary dependencies using:
```bash
npm install
```

and then build the bindings for `node.js` with
and build the bindings for `node.js` with

```bash
npm run build:nodejs
Expand Down
60 changes: 52 additions & 8 deletions bindings/wasm/build/lints.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@

// Aborts the build process if disallowed occurences are found in identity_wasm.js
/** Aborts the build process if disallowed occurrences are found in identity_wasm.js **/
function lintBigInt(content) {
if (content.includes("BigInt64Array") || content.includes("BigUint64Array")) {
console.error("Build artifacts should not include BigInt64Array/BigUint64Array imports")
console.error("to ensure React Native/WebKit compatibility.")
console.error("Remove any u64 and i64 occurrence from the public Wasm interface.")
console.error("See: https://github.com/iotaledger/identity.rs/issues/362")
process.exit(1)
throw(
"Build artifacts should not include BigInt64Array/BigUint64Array imports\n" +
"to ensure React Native/WebKit compatibility.\n" +
"Remove any u64 and i64 occurrence from the public Wasm interface.\n" +
"See: https://github.com/iotaledger/identity.rs/issues/362\n"
);
}
}

/**
* Rejects any `<obj>.ptr = 0;` occurrence, excluding `this.ptr = 0;` in `free()` implementations.
*
* Prevents generated code that nulls out Wasm pointers without de-registering the finalizer, since they cause
* runtime errors during automatic garbage collection from WasmRefCell thinking the instance is still borrowed.
*
* Functions which take owned parameters cause this situation; the solution is to borrow and clone the parameter
* instead.
**/
function lintPtrNullWithoutFree(content) {
// Find line numbers of offending code.
const lines = content.split(/\r?\n/);
const matches = lines.flatMap(function (line, number) {
if (/(?<!this).ptr = 0;/.test(line)) {
return [(number + 1) + " " + line.trim()];
} else {
return [];
}
});
if (matches.length > 0) {
throw(`ERROR: generated Javascript should not include 'obj.ptr = 0;'.
When weak references are enabled with '--weak-refs', WasmRefCell in wasm-bindgen causes
runtime errors from automatic garbage collection trying to free objects taken as owned parameters.

Matches:
${matches}

SUGGESTION: change any exported functions which take an owned parameter (excluding flat enums) to use a borrow instead.
See: https://github.com/rustwasm/wasm-bindgen/pull/2677`);
}
}

/** Runs all custom lints on the generated code. Exits the process immediately with code 1 if any fail. **/
function lintAll(content) {
try {
lintBigInt(content);
lintPtrNullWithoutFree(content);
} catch (err) {
console.error("Custom lint failed!");
console.error(err);
process.exit(1);
}
}

exports.lintBigInt = lintBigInt;
exports.lintAll = lintAll;
29 changes: 14 additions & 15 deletions bindings/wasm/build/node.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
const path = require('path')
const fs = require('fs')
const { lintBigInt } = require('./lints')
const { lintAll } = require('./lints')

// Add node fetch stuff (https://github.com/seanmonstar/reqwest/issues/910)
const entryFilePathNode = path.join(__dirname, '../node/identity_wasm.js')
const entryFileNode = fs.readFileSync(entryFilePathNode).toString()
const entryFilePathNode = path.join(__dirname, '../node/identity_wasm.js');
const entryFileNode = fs.readFileSync(entryFilePathNode).toString();

lintBigInt(entryFileNode);
lintAll(entryFileNode);

// Add node-fetch polyfill (https://github.com/seanmonstar/reqwest/issues/910).
let changedFileNode = entryFileNode.replace(
"let imports = {};",
`if (!globalThis.fetch) {
const fetch = require('node-fetch')
globalThis.Headers = fetch.Headers
globalThis.Request = fetch.Request
globalThis.Response = fetch.Response
globalThis.fetch = fetch
}
let imports = {};`
)
const fetch = require('node-fetch')
globalThis.Headers = fetch.Headers
globalThis.Request = fetch.Request
globalThis.Response = fetch.Response
globalThis.fetch = fetch
}
let imports = {};`);

fs.writeFileSync(
entryFilePathNode,
changedFileNode
)

);
30 changes: 15 additions & 15 deletions bindings/wasm/build/web.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
const path = require('path')
const fs = require('fs')
const { lintBigInt } = require('./lints')
const { lintAll } = require('./lints')

const entryFilePath = path.join(__dirname, '../web/identity_wasm.js')
const entryFile = fs.readFileSync(entryFilePath).toString()
const entryFilePath = path.join(__dirname, '../web/identity_wasm.js');
const entryFile = fs.readFileSync(entryFilePath).toString();

lintBigInt(entryFile);
lintAll(entryFile);

let changedFile = entryFile
// Comment out generated code as a workaround for webpack (does not recognise import.meta)
// Regex to avoid hard-coding 'identity_wasm_bg.wasm'
// Comment out generated code as a workaround for webpack (does not recognise import.meta).
// Regex to avoid hard-coding 'identity_wasm_bg.wasm'.
.replace(
/input = new URL\((.*), import\.meta\.url\);/i,
"// input = new URL($1, import.meta.url);"
)
// Rename original init function, because we want to use the name for our own function
// Rename original init function, because we want to use the name for our own function.
.replace(
"async function init(input) {",
"async function initWasm(input) {"
Expand All @@ -23,25 +23,25 @@ let changedFile = entryFile
"init.__wbindgen_wasm_module = module;",
"initWasm.__wbindgen_wasm_module = module;"
)
// Create an init function which imports the wasm file
// Create an init function which imports the wasm file.
.replace(
"export default init;",
"let __initializedIotaWasm = false\r\n\r\nexport function init(path) {\r\n if (__initializedIotaWasm) {\r\n return Promise.resolve(wasm)\r\n }\r\n return initWasm(path || \'identity_wasm_bg.wasm\').then(() => {\r\n __initializedIotaWasm = true\r\n return wasm\r\n })\r\n}\r\n"
)
);

fs.writeFileSync(
entryFilePath,
changedFile
)
);

const entryFilePathTs = path.join(__dirname, '../web/identity_wasm.d.ts')
const entryFileTs = fs.readFileSync(entryFilePathTs).toString()
// Replace the init function in the ts file
const entryFilePathTs = path.join(__dirname, '../web/identity_wasm.d.ts');
const entryFileTs = fs.readFileSync(entryFilePathTs).toString();
// Replace the init function in the ts file.
let changedFileTs = entryFileTs.replace(
"/**\n* If `module_or_path` is {RequestInfo} or {URL}, makes a request and\n* for everything else, calls `WebAssembly.instantiate` directly.\n*\n* @param {InitInput | Promise<InitInput>} module_or_path\n*\n* @returns {Promise<InitOutput>}\n*/\nexport default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;",
"\/**\r\n* Loads the Wasm file so the lib can be used, relative path to Wasm file\r\n* @param {string | undefined} path\r\n*\/\r\nexport function init (path?: string): Promise<void>;"
)
);
fs.writeFileSync(
entryFilePathTs,
changedFileTs
)
);
43 changes: 32 additions & 11 deletions bindings/wasm/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,15 +1021,19 @@ Defines the difference between two DID `Document`s' JSON representations.
**Kind**: global class

* [DiffMessage](#DiffMessage)
* [.did](#DiffMessage+did) ⇒ [<code>DID</code>](#DID)
* [.diff](#DiffMessage+diff) ⇒ <code>string</code>
* [.messageId](#DiffMessage+messageId) ⇒ <code>string</code>
* [.messageId](#DiffMessage+messageId)
* [.previousMessageId](#DiffMessage+previousMessageId) ⇒ <code>string</code>
* [.previousMessageId](#DiffMessage+previousMessageId)
* [.proof](#DiffMessage+proof) ⇒ <code>any</code>
* [.id()](#DiffMessage+id) ⇒ [<code>DID</code>](#DID)
* [.merge(document)](#DiffMessage+merge) ⇒ [<code>Document</code>](#Document)
* _instance_
* [.did](#DiffMessage+did) ⇒ [<code>DID</code>](#DID)
* [.diff](#DiffMessage+diff) ⇒ <code>string</code>
* [.messageId](#DiffMessage+messageId) ⇒ <code>string</code>
* [.messageId](#DiffMessage+messageId)
* [.previousMessageId](#DiffMessage+previousMessageId) ⇒ <code>string</code>
* [.previousMessageId](#DiffMessage+previousMessageId)
* [.proof](#DiffMessage+proof) ⇒ <code>any</code>
* [.id()](#DiffMessage+id) ⇒ [<code>DID</code>](#DID)
* [.merge(document)](#DiffMessage+merge) ⇒ [<code>Document</code>](#Document)
* [.toJSON()](#DiffMessage+toJSON) ⇒ <code>any</code>
* _static_
* [.fromJSON(json)](#DiffMessage.fromJSON) ⇒ [<code>DiffMessage</code>](#DiffMessage)

<a name="DiffMessage+did"></a>

Expand Down Expand Up @@ -1105,6 +1109,23 @@ with the given Document.
| --- | --- |
| document | [<code>Document</code>](#Document) |

<a name="DiffMessage+toJSON"></a>

### diffMessage.toJSON() ⇒ <code>any</code>
Serializes a `DiffMessage` as a JSON object.

**Kind**: instance method of [<code>DiffMessage</code>](#DiffMessage)
<a name="DiffMessage.fromJSON"></a>

### DiffMessage.fromJSON(json) ⇒ [<code>DiffMessage</code>](#DiffMessage)
Deserializes a `DiffMessage` from a JSON object.

**Kind**: static method of [<code>DiffMessage</code>](#DiffMessage)

| Param | Type |
| --- | --- |
| json | <code>any</code> |

<a name="Document"></a>

## Document
Expand Down Expand Up @@ -1613,7 +1634,7 @@ For a document with DID: did:iota:1234567890abcdefghijklmnopqrstuvxyzABCDEFGHI,
<a name="Document+toJSON"></a>

### document.toJSON() ⇒ <code>any</code>
Serializes a `Document` object as a JSON object.
Serializes a `Document` as a JSON object.

**Kind**: instance method of [<code>Document</code>](#Document)
<a name="Document.fromVerificationMethod"></a>
Expand Down Expand Up @@ -1672,7 +1693,7 @@ This is the Base58-btc encoded SHA-256 digest of the hex-encoded message id.
<a name="Document.fromJSON"></a>

### Document.fromJSON(json) ⇒ [<code>Document</code>](#Document)
Deserializes a `Document` object from a JSON object.
Deserializes a `Document` from a JSON object.

**Kind**: static method of [<code>Document</code>](#Document)

Expand Down
Loading