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

Fix: Issue #561 "LinkError: WebAssembly.Instantiate..." #562

Merged
merged 4 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"lib_compiler_noNodeModules": "could not locate {folder} in parent directories of web3api manifest",
"lib_compiler_noInvoke": "WASM module is missing the _w3_invoke export. This should never happen...",
"lib_compiler_dup_code_folder": "Duplicate code generation folder found `{directory}`. Please ensure each module file is located in a unique directory.",
"lib_compiler_missing_export": "{missingExport} is not exported from the WASM module `{moduleName}`",
"lib_compiler_invalid_module": "Invalid Wasm module found. `{moduleName}` at {modulePath} is invalid. Error: {error}",
"lib_compiler_cannotBuildInterfaceModules": "Cannot build modules for an Interface Web3API",
"lib_compiler_outputMetadataText": "Metadata written",
"lib_compiler_outputMetadataError": "Failed to output metadata",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"lib_compiler_noNodeModules": "could not locate {folder} in parent directories of web3api manifest",
"lib_compiler_noInvoke": "WASM module is missing the _w3_invoke export. This should never happen...",
"lib_compiler_dup_code_folder": "Duplicate code generation folder found `{directory}`. Please ensure each module file is located in a unique directory.",
"lib_compiler_missing_export": "{missingExport} is not exported from the WASM module `{moduleName}`",
"lib_compiler_invalid_module": "Invalid Wasm module found. `{moduleName}` at {modulePath} is invalid. Error: {error}",
"lib_compiler_cannotBuildInterfaceModules": "Cannot build modules for an Interface Web3API",
"lib_compiler_outputMetadataText": "Metadata written",
"lib_compiler_outputMetadataError": "Failed to output metadata",
Expand Down
61 changes: 21 additions & 40 deletions packages/cli/src/lib/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,12 @@ export class Compiler {
// Build the sources
const dockerImageId = await this._buildSourcesInDocker();

// Validate the WASM exports
// Validate the Wasm modules
await Promise.all(
Object.keys(modulesToBuild)
.filter((module: InvokableModules) => modulesToBuild[module])
.map((module: InvokableModules) =>
this._validateExports(module, outputDir)
this._validateWasmModule(module, outputDir)
)
);

Expand Down Expand Up @@ -528,16 +528,13 @@ export class Compiler {
}
}

private async _validateExports(
private async _validateWasmModule(
moduleName: InvokableModules,
buildDir: string
): Promise<void> {
const wasmSource = fs.readFileSync(
path.join(buildDir, `${moduleName}.wasm`)
);
const modulePath = path.join(buildDir, `${moduleName}.wasm`);
const wasmSource = fs.readFileSync(modulePath);

const mod = await WebAssembly.compile(wasmSource);
const memory = new WebAssembly.Memory({ initial: 1 });
const w3Imports: Record<keyof W3Imports, () => void> = {
__w3_subinvoke: () => {},
__w3_subinvoke_result_len: () => {},
Expand All @@ -553,40 +550,24 @@ export class Compiler {
__w3_abort: () => {},
};

const instance = await WebAssembly.instantiate(mod, {
env: {
memory,
},
w3: w3Imports,
});

const requiredExports = [
...WasmWeb3Api.requiredExports,
...AsyncWasmInstance.requiredExports,
];
const missingExports: string[] = [];

for (const requiredExport of requiredExports) {
if (!instance.exports[requiredExport]) {
missingExports.push(requiredExport);
}
}

if (missingExports.length) {
try {
const memory = AsyncWasmInstance.createMemory({ module: wasmSource });
await AsyncWasmInstance.createInstance({
module: wasmSource,
imports: {
env: {
memory,
},
w3: w3Imports,
},
requiredExports: WasmWeb3Api.requiredExports,
});
} catch (error) {
throw Error(
intlMsg.lib_compiler_missing_export({
missingExport: missingExports
.map((missingExport, index) => {
if (missingExports.length === 1) {
return missingExport;
} else if (index === missingExports.length - 1) {
return "& " + missingExport;
} else {
return missingExport + ", ";
}
})
.join(),
intlMsg.lib_compiler_invalid_module({
modulePath,
moduleName,
error,
})
);
}
Expand Down
74 changes: 57 additions & 17 deletions packages/js/asyncify/src/AsyncWasmInstance.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-empty-function */

type MaybeAsync<T> = Promise<T> | T;

function isPromise<T extends unknown>(
test?: MaybeAsync<T>
): test is Promise<T> {
return !!test && typeof (test as Promise<T>).then === "function";
}

function proxyGet<T extends Record<string, unknown>>(
obj: T,
transform: (value: unknown) => unknown
): T {
return new Proxy<T>(obj, {
get: (obj: T, name: string) => transform(obj[name]),
});
}
import { indexOfArray, isPromise, proxyGet } from "./utils";

type WasmMemory = WebAssembly.Memory;
type WasmExports = WebAssembly.Exports;
Expand Down Expand Up @@ -60,6 +44,62 @@ export class AsyncWasmInstance {

private constructor() {}

public static createMemory(config: { module: ArrayBuffer }): WasmMemory {
const bytecode = new Uint8Array(config.module);

// extract the initial memory page size, as it will
// throw an error if the imported page size differs:
// https://chromium.googlesource.com/v8/v8/+/644556e6ed0e6e4fac2dfabb441439820ec59813/src/wasm/module-instantiate.cc#924
const envMemoryImportSignature = Uint8Array.from([
// string length
0x03,
// env ; import module name
0x65,
0x6e,
0x76,
// string length
0x06,
// memory ; import field name
0x6d,
0x65,
0x6d,
0x6f,
0x72,
0x79,
// import kind
0x02,
// limits ; https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#resizable-limits
// limits ; flags
// 0x??,
// limits ; initial
// 0x__,
]);

const sigIdx = indexOfArray(bytecode, envMemoryImportSignature);

if (sigIdx < 0) {
throw Error(
`Unable to find Wasm memory import section. ` +
`Modules must import memory from the "env" module's ` +
`"memory" field like so:\n` +
`(import "env" "memory" (memory (;0;) #))`
);
}

// Extract the initial memory page-range size
const memoryInitalLimits = bytecode.at(
sigIdx + envMemoryImportSignature.length + 1
);

if (memoryInitalLimits === undefined) {
throw Error(
"No initial memory number found, this should never happen..."
);
}

return new WebAssembly.Memory({ initial: memoryInitalLimits });
}

public static async createInstance(config: {
module: ArrayBuffer;
imports: WasmImports;
Expand Down
51 changes: 51 additions & 0 deletions packages/js/asyncify/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export type MaybeAsync<T> = Promise<T> | T;

export function isPromise<T extends unknown>(
test?: MaybeAsync<T>
): test is Promise<T> {
return !!test && typeof (test as Promise<T>).then === "function";
}

export function proxyGet<T extends Record<string, unknown>>(
obj: T,
transform: (value: unknown) => unknown
): T {
return new Proxy<T>(obj, {
get: (obj: T, name: string) => transform(obj[name]),
});
}

export function indexOfArray(source: Uint8Array, search: Uint8Array): number {
let run = true;
let start = 0;

while (run) {
const idx = source.indexOf(search[0], start);

// not found
if (idx < start) {
run = false;
continue;
}

// Make sure the rest of the subarray contains the search pattern
const subBuff = source.subarray(idx, idx + search.length);

let retry = false;
let i = 1;
for (; i < search.length && !retry; ++i) {
if (subBuff.at(i) !== search.at(i)) {
retry = true;
}
}

if (retry) {
start = idx + i;
continue;
} else {
return idx;
}
}

return -1;
}
2 changes: 1 addition & 1 deletion packages/js/client/src/wasm/WasmWeb3Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class WasmWeb3Api extends Api {
);
};

const memory = new WebAssembly.Memory({ initial: 1 });
const memory = AsyncWasmInstance.createMemory({ module: wasm });
const instance = await AsyncWasmInstance.createInstance({
module: wasm,
imports: createImports({
Expand Down
9 changes: 9 additions & 0 deletions packages/js/plugins/http/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@ module.exports = {
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
modulePathIgnorePatterns: [
"<rootDir>/src/__tests__/e2e/integration/"
],
testPathIgnorePatterns: [
"<rootDir>/src/__tests__/e2e/integration/"
],
transformIgnorePatterns: [
"<rootDir>/src/__tests__/e2e/integration/"
],
testEnvironment: 'node'
}
1 change: 1 addition & 0 deletions packages/js/plugins/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"codegen": "node ../../../../dependencies/node_modules/@web3api/cli/bin/w3 plugin codegen",
"lint": "eslint --color -c ../../../../.eslintrc.js src/",
"test": "jest --passWithNoTests --runInBand --verbose",
"test:ci": "jest --passWithNoTests --runInBand --verbose",
"test:watch": "jest --watch --passWithNoTests --verbose"
},
"dependencies": {
Expand Down
Loading