javascriptnode.jsassemblyscript

AssemblyScript: Function working with i32, but not strings


I have an AssemblyScript function that returns any string it is given, as well as the corresponding code to import and run it in NodeJS:

AssemblyScript:

export function parse(x: string): string {
  return x;
}

NodeJS:

import { readFileSync } from 'fs';

export default async function compile(raw) {
  return WebAssembly.instantiate(
    readFileSync('./src/.assembly/engine.wasm') // The WASM file containing compiled AssemblyScript
  ).then(mod => {
    const { parse } = mod.instance.exports;
    return parse(raw);
  });
}

compile('test').then(res => {
  console.log(res);
});

However, when this code is run, it returns an error about the imports argument not being present:

Terminal:

node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[TypeError: WebAssembly.instantiate(): Imports argument must be present and must be an object]

Node.js v18.15.0
error Command failed with exit code 1.

Strangely, the code runs just fine if it uses i32 instead of string:

AssemblyScript:

export function parse(x: i32): i32 {
  return x;
}

NodeJS:

import { readFileSync } from 'fs';

export default async function compile(raw) {
  return WebAssembly.instantiate(
    readFileSync('./src/.assembly/engine.wasm') // The WASM file containing compiled AssemblyScript
  ).then(mod => {
    const { parse } = mod.instance.exports;
    return parse(raw);
  });
}

compile(44).then(res => {
  console.log(res);
});

Terminal:

44

How can I fix the code to work with strings too?


Solution

  • Your punctual error comes from the fact that, when strings are involved, AssemblyScript does a lot more to accommodate them, as they are not in the WebAssembly spec, which makes i32s much easier to work with.

    You can see a glimpse of this if you use the strings command to.

    i32:

    $ strings release.wasm
    parse
    memory
    sourceMappingURL
    ./release.wasm.map
    

    string:

    $ strings release.wasm
    abort
    __new
    __pin
    __unpin
    __collect
    __rtti_base
    memory
    parse
    A|q!
    ...many more lines later...
    A}q6
    A|qA
    sourceMappingURL
    ./release.wasm.map
    

    So, what WebAssembly.intantiate complains about is firstly, the lack of the imports object, and secondly, the lack of the abort function that it needs.

    Luckily, for a solution, you don't need to worry about any of that, the AssemblyScript loader does things automatically for you. You can use it to instantiate the module, instead of the WebAssembly.instantiate function:

    import { readFileSync } from 'fs';
    import loader from "@assemblyscript/loader";
    
    export default function compile(raw) {
      let wasmModule = loader.instantiateSync(readFileSync('./build/release.wasm'), {});
      const { __newString, __getString } = wasmModule.exports;
      return __getString(wasmModule.exports.parse(__newString(raw)));
    }
    
    console.log(compile('test'));
    

    Apart from that, the way to communicate with strings from javascript is sadly still to use the __newString and __getString that do the work of translating WebAssembly addresses for you, as WebAssembly does not have the concept of stings.

    And, for a bit of good news:

    $ node index.js 
    test
    

    Your code now works as intended.