Difference between revisions of "Generating WebAssembly with LDC"
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>): | ||
− | < | + | <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: | ||
− | + | <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: | ||
− | < | + | <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'); |
− | + | 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(); |
− | 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> | ||
− | + | == 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: | ||
+ | |||
+ | <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.
Contents
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
- Image Dithering with WebAssembly, D and LDC by Allen Garvey, Demo
- Meta tic-tac-toe game, Demo