From 0d66635b0b08563f977e642aebae31b168b3509f Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Tue, 10 Nov 2020 12:35:34 -0800 Subject: [PATCH 1/2] Allowed forcing the return type on a bound export --- README.md | 16 +++++ lib/asbind-instance/bind-function.js | 23 +++++-- lib/asbind-instance/supported-ref-types.js | 16 ++++- lib/lib.js | 6 +- package-lock.json | 6 +- test/test.js | 70 +++++++++++++--------- 6 files changed, 97 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 4c9a703..fd33c24 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ asyncTask(); _Did the quick start not work for you, or you are noticing some weird behavior? Please see the [FAQ and Common Issues](#faq-and-common-issues)_ +_Want to use `as-bind` in production? Please see the [Production section in the FAQ and Common Issues](#production)_ + ## Additional Examples ## Passing a high-level type to a an exported function, and returning a high-level type @@ -193,6 +195,12 @@ The `AsBind` class is meant to vaugely act as the [WebAssembly](https://develope Value that is the current version of your imported AsBind. +##### RETURN_TYPES + +`AsBind.RETURN_TYPES` + +Constants represented as JSON, for forcing the return type on [bound export functions](#exports). + ##### instantiate ```typescript @@ -239,6 +247,8 @@ Each **exported function** has the properties: - `shouldCacheTypes` - If you would like to disable type caching (speculative execution) for a particular function, you can do: `asBindInstance.exports.myFunction.shouldCacheTypes = false;`. Or set to true, to re-enable type caching. + - (Reccomended for production usage) Set this value on a bound export function, to force it's return type. This should be set to a constant found on: [`AsBind.RETURN_TYPES`](#return_types). Defaults to `null`. +- `returnType` - `unsafeReturnValue` - By default, all values (in particular [TypedArrays](https://www.assemblyscript.org/stdlib/typedarray.html#typedarray)) will be copied out of Wasm Memory, instead of giving direct read/write access. If you would like to use a view of the returned memory, you can do: `asBindInstance.exports.myFunction.unsafeReturnValue = true;`. For More context, please see the [AssemblyScript loader documentation](https://www.assemblyscript.org/loader.html#module-instance-utility) on array views. - After settings this flag on a function, it will then return it's values wrapped in an object, like so: `{ptr: /* The pointer or index in wasm memory the view is reffering to */, value: /* The returned value (TypedArray) that is backed directly by Wasm Memory */}` @@ -301,6 +311,12 @@ Eventually for the most performant option, we would want to do some JavaScript c In the future, these types of high-level data passing tools will not be needed for WebAssembly toolchains, once the [WebAssembly Inteface Types proposal](https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types/Explainer.md) lands, and this functionality is handled by the runtime / toolchain. +## Production + +`as-bind` works by abstracting away using the [AssemblyScript Loader](https://www.assemblyscript.org/loader.html). For passing values into your AssemblyScript, it uses the Loader on your half to allocate memory, and then passes the pointer to the allocated memory. However, to pass a value back from AssemblyScript to JavaScript, AsBind will iterate through all the supported types until it finds a match (or doesn't in which case it just returns the number). However, returning a value _can sometimes_ conflict with something in AssemblyScript memory, as discovered in [#50](https://github.com/torch2424/as-bind/issues/50). + +Thus, for production usage we highly reccomend that you set the [`returnType` property on your bound export functions](#exports) to ensure that this conflict does not happen. 😄 + ## Projects using as-bind - The as-bind example is a Markdown Parser, in which as-bind takes in a string, passes it to a rough markdown parser / compiler written in AssemblyScript, and returns a string. [(Live Demo)](https://torch2424.github.io/as-bind/), [(Source Code)](https://github.com/torch2424/as-bind/tree/master/examples/markdown-parser) diff --git a/lib/asbind-instance/bind-function.js b/lib/asbind-instance/bind-function.js index 3539811..d28936e 100644 --- a/lib/asbind-instance/bind-function.js +++ b/lib/asbind-instance/bind-function.js @@ -1,5 +1,5 @@ import { validateExportsAndFunction } from "./validate"; -import SUPPORTED_REF_TYPES from "./supported-ref-types"; +import { SUPPORTED_REF_TYPES } from "./supported-ref-types"; // Function that takes in an asbindInstance, and importObject, and the path to the import function on the object // (E.g ["env", "myObject", "myFunction"] for {env: myObject: {myFunction: () => {}}}) @@ -195,13 +195,21 @@ export function bindExportFunction(asbindInstance, exportFunctionKey) { // Find our supported type let supportedType = undefined; - // Check if we cached the return type - if (functionThis.shouldCacheTypes && functionThis.cachedReturnTypes[0]) { + if (functionThis.returnType) { + // Check if the return type was manually set + if (SUPPORTED_REF_TYPES[functionThis.returnType]) { + supportedType = SUPPORTED_REF_TYPES[functionThis.returnType]; + } + } else if ( + functionThis.shouldCacheTypes && + functionThis.cachedReturnTypes[0] + ) { + // Check if we cached the return type + if (functionThis.cachedReturnTypes[0].type === "ref") { - supportedType = supportedType = + supportedType = SUPPORTED_REF_TYPES[functionThis.cachedReturnTypes[0].key]; } - // Let it fall through the if and handle the primitive (number) logic } else { // We need to find / cache the type Object.keys(SUPPORTED_REF_TYPES).some(key => { @@ -242,7 +250,9 @@ export function bindExportFunction(asbindInstance, exportFunctionKey) { } } else if (typeof exportFunctionResponse === "number") { response = exportFunctionResponse; - if (functionThis.shouldCacheTypes) { + // Need to check if we are caching types + // And, if the type was forced to a number, and we fell through, don't cache it + if (functionThis.shouldCacheTypes && !functionThis.returnType) { functionThis.cachedReturnTypes[0] = { type: "number" }; @@ -257,6 +267,7 @@ export function bindExportFunction(asbindInstance, exportFunctionKey) { // Initialize the state of our function boundExport.shouldCacheTypes = true; boundExport.unsafeReturnValue = false; + boundExport.returnType = null; boundExport.cachedArgTypes = []; boundExport.cachedReturnTypes = []; diff --git a/lib/asbind-instance/supported-ref-types.js b/lib/asbind-instance/supported-ref-types.js index c9bf619..0cf6adb 100644 --- a/lib/asbind-instance/supported-ref-types.js +++ b/lib/asbind-instance/supported-ref-types.js @@ -5,7 +5,7 @@ const getUnsafeResponse = (value, ptr) => { }; }; -const SUPPORTED_REF_TYPES = { +export const SUPPORTED_REF_TYPES = { STRING: { isTypeFromArgument: arg => { return typeof arg === "string"; @@ -204,4 +204,16 @@ const SUPPORTED_REF_TYPES = { } }; -export default SUPPORTED_REF_TYPES; +// Our return type constant +export const RETURN_TYPES = { + NUMBER: "NUMBER", + STRING: "STRING", + INT8ARRAY: "INT8ARRAY", + UINT8ARRAY: "UINT8ARRAY", + INT16ARRAY: "INT16ARRAY", + UINT16ARRAY: "UINT16ARRAY", + INT32ARRAY: "INT32ARRAY", + UINT32ARRAY: "UINT32ARRAY", + FLOAT32ARRAY: "FLOAT32ARRAY", + FLOAT64ARRAY: "FLOAT64ARRAY" +}; diff --git a/lib/lib.js b/lib/lib.js index 6bdefba..f62e8f4 100644 --- a/lib/lib.js +++ b/lib/lib.js @@ -1,10 +1,14 @@ import packageJson from "../package.json"; import AsbindInstance from "./asbind-instance/asbind-instance"; +import { RETURN_TYPES } from "./asbind-instance/supported-ref-types"; export const AsBind = { - // General asbind versionn + // General asbind version version: packageJson.version, + // Constants + RETURN_TYPES: RETURN_TYPES, + // Our APIs instantiate: async (source, importObject) => { let asbindInstance = new AsbindInstance(); diff --git a/package-lock.json b/package-lock.json index e938b6a..e450a94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,9 +30,9 @@ } }, "@assemblyscript/loader": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.14.4.tgz", - "integrity": "sha512-NFst3IvxSf08dkas4cWhGdl+Np56Yts9SZVbvTpOiLs3DVk1ZWg14ZFBgje3A/ztfLFeJ3F46APlr+ArRllpXQ==", + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.17.2.tgz", + "integrity": "sha512-8pLMRIMqPVxOnlyqjflNRuvoHShYNDCeUI3VXN6dr+c3lD4W5lwQnRdJviku8U4opv7CSDfIDZ99uRTwqcBIIA==", "dev": true }, "@babel/code-frame": { diff --git a/test/test.js b/test/test.js index bba12e9..7e7e1b4 100644 --- a/test/test.js +++ b/test/test.js @@ -626,36 +626,9 @@ describe("asbind", () => { describe("Unsafe Return Value", () => { let asbindInstance; - let testImportCalledWith = []; beforeEach(async () => { - const importObjectFunction = value => { - testImportCalledWith = [value]; - }; - - wrappedBaseImportObject = { - ...baseImportObject, - test: { - testImportString: importObjectFunction, - testImportTwoStrings: (value1, value2) => { - testImportCalledWith = [value1, value2]; - }, - testImportReturnNumber: () => -1, - testImportInt8Array: importObjectFunction, - testImportUint8Array: importObjectFunction, - testImportInt16Array: importObjectFunction, - testImportUint16Array: importObjectFunction, - testImportInt32Array: importObjectFunction, - testImportUint32Array: importObjectFunction, - testImportFloat32Array: importObjectFunction, - testImportFloat64Array: importObjectFunction - } - }; - - asbindInstance = await AsBind.instantiate( - wasmBytes, - wrappedBaseImportObject - ); + asbindInstance = await AsBind.instantiate(wasmBytes, baseImportObject); }); it("should not break strings", () => { @@ -706,4 +679,45 @@ describe("asbind", () => { }); }); }); + + describe("Forcing Return Types", () => { + let asbindInstance; + + beforeEach(async () => { + asbindInstance = await AsBind.instantiate(wasmBytes, baseImportObject); + }); + + it("should allow setting the returnType on a bound export function", () => { + // Make sure the return type is null + assert.equal(asbindInstance.exports.helloWorld.returnType, null); + + // Call the export + const defaultResponse = asbindInstance.exports.helloWorld("returnType"); + assert.equal(typeof defaultResponse, "string"); + + // Set the return type to a number + asbindInstance.exports.helloWorld.returnType = AsBind.RETURN_TYPES.NUMBER; + + // Call the export + const numberResponse = asbindInstance.exports.helloWorld("returnType"); + assert.equal(typeof numberResponse, "number"); + + // Set the return type to a string + asbindInstance.exports.helloWorld.returnType = AsBind.RETURN_TYPES.STRING; + + // Call the export + const stringResponse = asbindInstance.exports.helloWorld("returnType"); + assert.equal(typeof stringResponse, "string"); + + // Remove the returnType + asbindInstance.exports.helloWorld.returnType = null; + + // Call the export + const nullReturnTypeResponse = asbindInstance.exports.helloWorld( + "returnType" + ); + assert.equal(typeof nullReturnTypeResponse, "string"); + }); + }); + // Done! }); From 18d476f91da5104dfc18609d1d05c3327d933b5b Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Tue, 10 Nov 2020 12:40:21 -0800 Subject: [PATCH 2/2] Fixed the docs --- README.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd33c24..b5ad7c9 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,21 @@ Value that is the current version of your imported AsBind. `AsBind.RETURN_TYPES` -Constants represented as JSON, for forcing the return type on [bound export functions](#exports). +Constants (represented as JSON) of the supported return types on bound export functions. This is useful for forcing the return type on [bound export functions](#exports). + +For example, this could be used like: + +```typescript +// Force our return type to our expected string +asBindInstance.exports.myExportedFunctionThatReturnsAString.returnType = + AsBind.RETURN_TYPES.STRING; +const myString = asBindInstance.exports.myExportedFunctionThatReturnsAString(); + +// Force our return type to return a number (The pointer to the string) +asBindInstance.exports.myExportedFunctionThatReturnsAString.returnType = + AsBind.RETURN_TYPES.NUMBER; +const myNumber = asBindInstance.exports.myExportedFunctionThatReturnsAString(); +``` ##### instantiate @@ -247,8 +261,8 @@ Each **exported function** has the properties: - `shouldCacheTypes` - If you would like to disable type caching (speculative execution) for a particular function, you can do: `asBindInstance.exports.myFunction.shouldCacheTypes = false;`. Or set to true, to re-enable type caching. - - (Reccomended for production usage) Set this value on a bound export function, to force it's return type. This should be set to a constant found on: [`AsBind.RETURN_TYPES`](#return_types). Defaults to `null`. - `returnType` + - (Reccomended for production usage) Set this value on a bound export function, to force it's return type. This should be set to a constant found on: [`AsBind.RETURN_TYPES`](#return_types). Defaults to `null`. - `unsafeReturnValue` - By default, all values (in particular [TypedArrays](https://www.assemblyscript.org/stdlib/typedarray.html#typedarray)) will be copied out of Wasm Memory, instead of giving direct read/write access. If you would like to use a view of the returned memory, you can do: `asBindInstance.exports.myFunction.unsafeReturnValue = true;`. For More context, please see the [AssemblyScript loader documentation](https://www.assemblyscript.org/loader.html#module-instance-utility) on array views. - After settings this flag on a function, it will then return it's values wrapped in an object, like so: `{ptr: /* The pointer or index in wasm memory the view is reffering to */, value: /* The returned value (TypedArray) that is backed directly by Wasm Memory */}`