Difference between revisions of "Generating WebAssembly with LDC"

From D Wiki
Jump to: navigation, search
m (Test in HTML page)
m (-bl)
 
(15 intermediate revisions by 6 users not shown)
Line 5: Line 5:
 
Let's generate a .wasm file for this D code (<tt>wasm.d</tt>):
 
Let's generate a .wasm file for this D code (<tt>wasm.d</tt>):
  
<pre>
+
<syntaxhighlight lang="D">
 
extern(C): // disable D mangling
 
extern(C): // disable D mangling
  
Line 12: Line 12:
 
// seems to be the required entry point
 
// seems to be the required entry point
 
void _start() {}
 
void _start() {}
</pre>
+
</syntaxhighlight>
 +
 
 +
Build <tt>wasm.wasm</tt>:
 +
 
 +
<pre>ldc2 -mtriple=wasm32-unknown-unknown-wasm -betterC wasm.d</pre>
 +
 
 +
If using DUB:
  
Invoke <tt>ldc2 -mtriple=wasm32-unknown-unknown-wasm -betterC -link-internally wasm.d</tt>, this generates a <tt>wasm.wasm</tt> file.
+
<pre>dub build --compiler=ldc2 --arch=wasm32-unknown-unknown-wasm</pre>
  
 
In case LDC errors out (e.g., with unsupported <tt>-link-internally</tt>), try an [https://github.com/ldc-developers/ldc/releases/ official prebuilt release package].
 
In case LDC errors out (e.g., with unsupported <tt>-link-internally</tt>), try an [https://github.com/ldc-developers/ldc/releases/ official prebuilt release package].
Line 22: Line 28:
 
Let's test it with a little HTML page, loading and invoking the WebAssembly via JavaScript. Generate an .html file in the same directory as the .wasm file, with the following contents:
 
Let's test it with a little HTML page, loading and invoking the WebAssembly via JavaScript. Generate an .html file in the same directory as the .wasm file, with the following contents:
  
<pre>
+
<syntaxhighlight lang="HTML">
 
<html>
 
<html>
 
   <head>
 
   <head>
 
     <script>
 
     <script>
 
+
      const request = new XMLHttpRequest();
const request = new XMLHttpRequest();
+
      request.open('GET', 'wasm.wasm');
request.open('GET', 'wasm.wasm');
+
      request.responseType = 'arraybuffer';
request.responseType = 'arraybuffer';
+
      request.onload = () => {
request.onload = () => {
+
        console.log('response received');
  console.log('response received');
+
        const bytes = request.response;
  const bytes = request.response;
+
        const importObject = {};
  const importObject = {};
+
        WebAssembly.instantiate(bytes, importObject).then(result => {
  WebAssembly.instantiate(bytes, importObject).then(result => {
+
          console.log('instantiated');
    console.log('instantiated');
+
          const { exports } = result.instance;
    const { exports } = result.instance;
+
          // finally, call the add() function implemented in D:
    // finally, call the add() function implemented in D:
+
          const r = exports.add(42, -2.5);
    const r = exports.add(42, -2.5);
+
          console.log('r = ' + r);
    console.log('r = ' + r);
+
        });
  });
+
      };
};
+
      request.send();
request.send();
+
      console.log('request sent');
console.log('request sent');
 
 
 
 
     </script>
 
     </script>
 
   </head>
 
   </head>
Line 51: Line 55:
 
   </body>
 
   </body>
 
</html>
 
</html>
 +
</syntaxhighlight>
 +
 +
Note that <tt>fetch()</tt> doesn't work for files in the local filesystem (<tt>file://</tt>), but <tt>XMLHttpRequest</tt> does in Firefox (not in Chrome though IIRC).
 +
 +
Open the HTML page; the JavaScript console should show:
 +
 +
<pre>
 +
request sent
 +
response received
 +
instantiated
 +
r = 39.5
 
</pre>
 
</pre>
  
Note that <tt>fetch()</tt> doesn't work for files in the local filesystem, but <tt>XMLHttpRequest</tt> does in Firefox (not in Chrome though IIRC).
+
== Calling external functions ==
Open the HTML page; the JavaScript console should show:
+
 
 +
The minimal example above only calls in one direction, from JavaScript to WebAssembly. Here's how to call external functions in D:
 +
 
 +
<tt>wasm.d</tt>:
 +
 
 +
<syntaxhighlight lang="D">
 +
extern(C): // disable D mangling
 +
 
 +
// import a function "callback" from default import module name "env"
 +
void callback(double a, double b, double c);
 +
 
 +
double add(double a, double b)
 +
{
 +
    const c = a + b;
 +
    callback(a, b, c);
 +
    return c;
 +
}
 +
 
 +
void _start() {}
 +
</syntaxhighlight>
 +
 
 +
Add <tt>-L-allow-undefined</tt> as linker flag to the LDC command line, otherwise LLD refuses to link due to undefined <tt>callback()</tt>.
 +
 
 +
Implement the <tt>callback()</tt> function in JavaScript and specify it in <tt>importObject.env</tt>:
 +
 
 +
<syntaxhighlight lang="JavaScript">
 +
const callback = (a, b, c) => {
 +
  console.log(`callback from D: ${a} + ${b} = ${c}`);
 +
};
 +
 
 +
// ...
 +
 
 +
  const importObject = {
 +
    env: { callback }
 +
  };
 +
</syntaxhighlight>
 +
 
 +
The log should now show:
  
 
<pre>
 
<pre>
Line 60: Line 112:
 
response received
 
response received
 
instantiated
 
instantiated
 +
callback from D: 42 + -2.5 = 39.5
 
r = 39.5
 
r = 39.5
 
</pre>
 
</pre>
  
[[Category:LDC]]
+
To import functions from other modules or rename imported functions, we use LDC's <tt>@llvmAttr</tt>:
 +
 
 +
<syntaxhighlight lang="D">
 +
import ldc.attributes;
 +
 
 +
extern(C): // disable D mangling
 +
 
 +
// import a function "add" from module name "math" and rename it to "add_numbers"
 +
@llvmAttr("wasm-import-module", "math") @llvmAttr("wasm-import-name", "add") {
 +
int add_numbers(int a, int b);
 +
}
 +
 
 +
// export a function "hello"
 +
export int hello(int a, int b, int c)
 +
{
 +
int s1 = add_numbers(a, b);
 +
int s2 = add_numbers(s1, c);
 +
return s2;
 +
}
 +
 
 +
void _start() {}</syntaxhighlight>
 +
 
 +
=== Calling conventions ===
 +
 
 +
==== Arrays ====
 +
 
 +
When calling an external function from web assembly, arrays are passed as two separate arguments: the length followed by the pointer (offset into linear memory) to the array data. For example, if an external function is declared as:
 +
 
 +
<syntaxhighlight lang="D">
 +
void externalFunc(int a, string b, float c);
 +
</syntaxhighlight>
 +
 
 +
The corresponding Javascript function should be declared this way:
 +
 
 +
<syntaxhighlight lang="JavaScript">
 +
function externalFunc(a, bLength, bOffset, c) { ... }
 +
</syntaxhighlight>
 +
 
 +
The array data can be accessed from Javascript by creating the appropriate typed array over the web assembly module's linear memory. For example, a passed string can be retrieved in Javascript like this:
 +
 
 +
<syntaxhighlight lang="JavaScript">
 +
function externalFunc(a, bLength, bOffset, c) {
 +
  // result is the object passed by WebAssembly.instantiate to the callback.
 +
  bytes = new Uint8Array(result.instance.memory.buffer, bOffset, bLength);
 +
  str = TextDecoder('utf8').decode(bytes);
 +
}
 +
</syntaxhighlight>
 +
 
 +
==== Non-POD parameters ====
 +
 
 +
Non-POD values are passed by reference to external functions. I.e., the external function will receive a pointer (offset) to the value, and must read the linear memory at that offset to retrieve the data.
 +
 
 +
==== Non-POD return values ====
 +
 
 +
Non-POD return values are implemented by passing a pointer as the first argument of the external function. I.e.,
 +
 
 +
<syntaxhighlight lang="D">
 +
struct MyStruct { int x; }
 +
MyStruct externalFunc(int x);
 +
</syntaxhighlight>
 +
 
 +
translates to the Javascript function:
 +
 
 +
<syntaxhighlight lang="JavaScript">
 +
function MyStruct(resultOffset, x) {
 +
  // caller expects the return value to be stored in linear memory at offset resultOffset.
 +
  // No return statement (behaves like a void function).
 +
}
 +
</syntaxhighlight>
 +
 
 +
== Additional LLVM wasm features ==
 +
 
 +
You can list the supported LLVM features for WASM with your LDC version like SIMD or exception handling using
 +
 
 +
<pre>ldc2 -mtriple=wasm32-unknown-unknown-wasm -mattr=help</pre>
 +
 
 +
'''Example output''': ('''may be outdated''', run the above command yourself to get the supported features for your LDC version)
 +
 
 +
<pre>
 +
Targeting wasm32. Available CPUs for this target:
 +
 
 +
  bleeding-edge - Select the bleeding-edge processor.
 +
  generic      - Select the generic processor.
 +
  mvp          - Select the mvp processor.
 +
 
 +
Available features for this target:
 +
 
 +
  atomics            - Enable Atomics.
 +
  bulk-memory        - Enable bulk memory operations.
 +
  exception-handling  - Enable Wasm exception handling.
 +
  multivalue          - Enable multivalue blocks, instructions, and functions.
 +
  mutable-globals    - Enable mutable globals.
 +
  nontrapping-fptoint - Enable non-trapping float-to-int conversion operators.
 +
  reference-types    - Enable reference types.
 +
  sign-ext            - Enable sign extension operators.
 +
  simd128            - Enable 128-bit SIMD.
 +
  tail-call          - Enable tail call instructions.
 +
 
 +
Use +feature to enable a feature, or -feature to disable it.
 +
For example, llc -mcpu=mycpu -mattr=+feature1,-feature2
 +
</pre>
 +
 
 +
== Conditional compilation ==
 +
 
 +
The version identifier version(WebAssembly) is defined when compiling for a web assembly target. This can be used for conditional compilation involving web assembly targets.
 +
 
 +
== Examples ==
 +
 
 +
* [https://github.com/allen-garvey/wasm-dither-example Image Dithering with WebAssembly, D and LDC] by Allen Garvey, [https://allen-garvey.github.io/wasm-dither-example/ Demo]
 +
* [https://github.com/dkorpel/tictac Meta tic-tac-toe game], [https://dkorpel.github.io/tictac/ Demo]
 +
 
 +
[[Category:LDC]] [[Category:WebAssembly]]

Latest revision as of 21:11, 31 January 2024

Starting with v1.11, LDC supports compiling and linking directly to WebAssembly. This page shows how to get started.

Building WebAssembly

Let's generate a .wasm file for this D code (wasm.d):

extern(C): // disable D mangling

double add(double a, double b) { return a + b; }

// seems to be the required entry point
void _start() {}

Build wasm.wasm:

ldc2 -mtriple=wasm32-unknown-unknown-wasm -betterC wasm.d

If using DUB:

dub build --compiler=ldc2 --arch=wasm32-unknown-unknown-wasm

In case LDC errors out (e.g., with unsupported -link-internally), try an official prebuilt release package.

Test in HTML page

Let's test it with a little HTML page, loading and invoking the WebAssembly via JavaScript. Generate an .html file in the same directory as the .wasm file, with the following contents:

<html>
  <head>
    <script>
      const request = new XMLHttpRequest();
      request.open('GET', 'wasm.wasm');
      request.responseType = 'arraybuffer';
      request.onload = () => {
        console.log('response received');
        const bytes = request.response;
        const importObject = {};
        WebAssembly.instantiate(bytes, importObject).then(result => {
          console.log('instantiated');
          const { exports } = result.instance;
          // finally, call the add() function implemented in D:
          const r = exports.add(42, -2.5);
          console.log('r = ' + r);
        });
      };
      request.send();
      console.log('request sent');
    </script>
  </head>
  <body>
    Test page
  </body>
</html>

Note that fetch() doesn't work for files in the local filesystem (file://), but XMLHttpRequest does in Firefox (not in Chrome though IIRC).

Open the HTML page; the JavaScript console should show:

request sent
response received
instantiated
r = 39.5

Calling external functions

The minimal example above only calls in one direction, from JavaScript to WebAssembly. Here's how to call external functions in D:

wasm.d:

extern(C): // disable D mangling

// import a function "callback" from default import module name "env"
void callback(double a, double b, double c);

double add(double a, double b)
{
    const c = a + b;
    callback(a, b, c);
    return c;
}

void _start() {}

Add -L-allow-undefined as linker flag to the LDC command line, otherwise LLD refuses to link due to undefined callback().

Implement the callback() function in JavaScript and specify it in importObject.env:

const callback = (a, b, c) => {
  console.log(`callback from D: ${a} + ${b} = ${c}`);
};

// ...

  const importObject = {
    env: { callback }
  };

The log should now show:

request sent
response received
instantiated
callback from D: 42 + -2.5 = 39.5
r = 39.5

To import functions from other modules or rename imported functions, we use LDC's @llvmAttr:

import ldc.attributes;

extern(C): // disable D mangling

// import a function "add" from module name "math" and rename it to "add_numbers"
@llvmAttr("wasm-import-module", "math") @llvmAttr("wasm-import-name", "add") { 
	int add_numbers(int a, int b);
}

// export a function "hello"
export int hello(int a, int b, int c)
{
	int s1 = add_numbers(a, b);
	int s2 = add_numbers(s1, c);
	return s2;
}

void _start() {}

Calling conventions

Arrays

When calling an external function from web assembly, arrays are passed as two separate arguments: the length followed by the pointer (offset into linear memory) to the array data. For example, if an external function is declared as:

void externalFunc(int a, string b, float c);

The corresponding Javascript function should be declared this way:

function externalFunc(a, bLength, bOffset, c) { ... }

The array data can be accessed from Javascript by creating the appropriate typed array over the web assembly module's linear memory. For example, a passed string can be retrieved in Javascript like this:

function externalFunc(a, bLength, bOffset, c) {
   // result is the object passed by WebAssembly.instantiate to the callback.
   bytes = new Uint8Array(result.instance.memory.buffer, bOffset, bLength);
   str = TextDecoder('utf8').decode(bytes);
}

Non-POD parameters

Non-POD values are passed by reference to external functions. I.e., the external function will receive a pointer (offset) to the value, and must read the linear memory at that offset to retrieve the data.

Non-POD return values

Non-POD return values are implemented by passing a pointer as the first argument of the external function. I.e.,

struct MyStruct { int x; }
MyStruct externalFunc(int x);

translates to the Javascript function:

function MyStruct(resultOffset, x) {
   // caller expects the return value to be stored in linear memory at offset resultOffset.
   // No return statement (behaves like a void function).
}

Additional LLVM wasm features

You can list the supported LLVM features for WASM with your LDC version like SIMD or exception handling using

ldc2 -mtriple=wasm32-unknown-unknown-wasm -mattr=help

Example output: (may be outdated, run the above command yourself to get the supported features for your LDC version)

Targeting wasm32. Available CPUs for this target:

  bleeding-edge - Select the bleeding-edge processor.
  generic       - Select the generic processor.
  mvp           - Select the mvp processor.

Available features for this target:

  atomics             - Enable Atomics.
  bulk-memory         - Enable bulk memory operations.
  exception-handling  - Enable Wasm exception handling.
  multivalue          - Enable multivalue blocks, instructions, and functions.
  mutable-globals     - Enable mutable globals.
  nontrapping-fptoint - Enable non-trapping float-to-int conversion operators.
  reference-types     - Enable reference types.
  sign-ext            - Enable sign extension operators.
  simd128             - Enable 128-bit SIMD.
  tail-call           - Enable tail call instructions.

Use +feature to enable a feature, or -feature to disable it.
For example, llc -mcpu=mycpu -mattr=+feature1,-feature2

Conditional compilation

The version identifier version(WebAssembly) is defined when compiling for a web assembly target. This can be used for conditional compilation involving web assembly targets.

Examples