2. Thanks to @yglukhov, we know we have to cross-compile to linux to make it work, especially for Windows. This can be used by putting the following "nim.cfg" file into your project directory:
@if emscripten:
cc = clang
@if windows:
clang.exe = "emcc.bat"
clang.linkerexe = "emcc.bat"
@else:
clang.exe = "emcc"
clang.linkerexe = "emcc"
@end
os = linux
cpu = i386
@if asmjs:
passC = "-s WASM=0 -s MODULARIZE=1"
passL = "-s WASM=0 -s MODULARIZE=1"
@else:
passC = "-s MODULARIZE=1"
passL = "-s MODULARIZE=1"
@end
@end
I've added the "MODULARIZE" commands to the compiler to generate easy-to-use code to be called when one wants to call back into wasm/asmjs. 3. Call the nim compiler with the following for compilation to wasm:
nim c -d:emscripten -d:danger -o:<path_to_output_file.js> <path_to_Nim_source_file>
or the following for compilation to asm.js:
nim c -d:emscripten -d:asmjs -d:danger -o:<path_to_output_file.js> <path_to_Nim_source_file>
Note that you need to set the emscripten SDK environment in your terminal prior to compiling with the "emsdk_env" command as per whatever platform you are on according to the emscripten installation instructions.
5. Any Nim code can now be run as a node application as long as it is just a console application; however, to be linked into an HTML web page, some more steps are required. As an example the following "demo.nim" file can be used:
# compiling to WebAssembly with emscripten...
#
# This function will change the HTML value of a node(or element) that has id=demo
#
# Important things to note:
#
# EMSCRIPTEN_KEEPALIVE
# It forces this function to be present in the WebAssembly module.
# There is no reference to this function in the module, the optimization would
# wipe it out as it appears to be dead code.
# This function is called from JavaScript though
#
# EM_ASM_
# EMSCRIPTEN macro that does all the interface with Javascript for us.
# It creates a function inside the generated javascript file and calls it from here.
# Generated function for the Javascript file:
# var ASM_CONSTS = [function($0) { document.getElementById("demo").value='Webassembly click count: '+$0 }];
#
# Global variable that holds how many times this function was clicked...
var clicks {.exportc.} = 0
{.emit: """#include <emscripten.h>""".}
import macros
macro EMSCRIPTEN_KEEPALIVE*(someProc: untyped) =
result = someProc
#[
Ident !"exportc"
ExprColonExpr
Ident !"codegenDecl"
StrLit __attribute__((used)) $# $#$#
]#
result.addPragma(newIdentNode("exportc"))
# emcc mangle cpp function names. This code fix it
when defined(cpp):
result.addPragma(newNimNode(nnkExprColonExpr).add(
newIdentNode("codegenDecl"),
newLit("__attribute__((used)) extern \"C\" $# $#$#")))
else:
result.addPragma(newNimNode(nnkExprColonExpr).add(
newIdentNode("codegenDecl"),
newLit("__attribute__((used)) $# $#$#")))
proc incrementClickCountOnValue*(): uint {.EMSCRIPTEN_KEEPALIVE.} =
# Modify the DOM, through a Javascript call provided by EM_ASM_, getElementById is the DOM API used
{.emit: """EM_ASM_( {document.getElementById("demo").innerHTML='Webassembly click count: '+$0}, ++clicks );""".}
result = 1
Note that I've borrowed the EMSCRIPTEN_KEEPALIVE macro from jsbind that makes our nim proc available to be called from JavaScript after it has been compiled to WebAssembly.
Also note that if objects that require memory management such as strings need to be transferred between JS and WASM, they will need to be copied disposed of by the Nim runtime, although "--gc:arc/orc" should make that much easier; however, using "--gc:none" and doing manual allocations and deallocations would work, too.
6. In the project root directly, also put a HTML file named "index.html" containing the following:
<!doctype html>
<html>
<head>
<script type="text/javascript" src="demo.js"></script>
<script>
// EMSCRIPTEN "-s MODULARIZE=1" option makes it easy to load a WebAssembly module,
// it generates a .js file with a Module() function that does the job.
// The .js file also puts some glue code that allows changing the DOM from within
// the WebAssembly code (through JavaScript)
function init() {
Module().then(function(instance){
let incrementClickCountOnValue = instance._incrementClickCountOnValue;
// Add button listener
let elem = document.getElementById('demo');
elem.addEventListener('click', function() {
// Finally, the wasm code is set up to be called here, it changes the button value!
incrementClickCountOnValue();
}, false);
} );
}
</script>
</head>
<body onLoad="init()">
<p id="demo">Click here to see wasm changing this message</p>
</body>
</html>
7. Compile the project from inside the folder with the following to use WASM:
nim c -d:emscripten -d:danger -o:demo.js demo
or the follwing to use asmjs:
nim c -d:emscripten -d:asmjs -d:danger -o:demo.js demo
7. Run the project from the project root directory with the following:
emrun index.html
This is necessary to serve the wasm code file which modern browsers won't allow to be accessed from a local device as a security measure, you can just open the asmjs version directly in a browser.
You can view a text version of the ".wasm" file (a ".wat" file) to see what was output using wasm2wat; which can help see what is exported and the names used as well as the actual WebAssembly assembly language used.
@treeform:
What are your thoughts on https://github.com/treeform/nim_emscripten_tutorial ?
I like your tutorial a lot, and it obviously goes much further than the basic steps I outline above; I do note that although your repo states that "step 4 now works" use of it has not been added to the README.md file.
To further the knowledge of how effective using WASM from Nim can be, I compiled my almost winning Nim contribution to the Software Drag Race, using the steps from the OP to find that without any modifications to the Nim code whatsoever!!!, the Emscripten compiled WASM code runs under node at about 80 percent of the speed of the compiled native code! Interestingly, the five different techniques used are all roughly the same ratio of performance of 80 percent between the WASM/native code...
Don't use i386 as your cpu target, use wasm32. This isn't documented anywhere unfortunately...i found out the hard way. Also i wouldn't use nim.cfg. Definitely take a look at treeform's tutorial. I'm building the web client for ansiwave the same way, with only slight modifications (using --opt:size is a good idea IMO).
People wrongly assume that targeting wasm is somehow off the beaten path for nim. The reality is that it's just a config file away, and it just works. There is no need for a complex ecosystem of tooling like you see in rust. Nim outputs C code, so it fits emscripten's tooling like a glove.
@sekao:
Don't use i386 as your cpu target, use wasm32.
Are you saying i386 didn't work for you?
I used i386 just as @yglukhov recommended and it as always worked for me; when I use wasm32 as a target for the Drag Race program, it compiles but I get some sort of memory allocation problem where the memory allocation is no longer emulated in Nim properly, meaning I would have to change the code to support it. This is likely because memory management in Nim is possibly automatically set to none for unrecognized targets. What kind of problems did you get using i386?
Are you setting --define:useMalloc? I haven't experienced memory errors with or without useMalloc but maybe it will help.
When using i386 an alignment fault was being thrown for me. What do you mean by unrecognized target? wasm32 is a target in the compiler.
My problem with wasm32 is that the allocated memory is never deallocated and memory use grows past the maximum point, where with i386 it can be automatically deallocated by Nim's memory management.
Yes, wasm32 is an accepted target, but beyond setting the flag, no further use is made of it, unlike the i386 flag.
It worked with default GC but using was32 as a target fixes the use of arc memory management, so it seems the extra things triggered by i386 aren't compatible with arc and likely orc. However, IIRC, that was with Nim version 1.4 8 and this seems to be fixed with version 1.6.2