Keno / julia-wasm

Running julia on wasm
https://keno.github.io/julia-wasm/website/repl.htm
332 stars 23 forks source link

Proper implementation of foreigncall in interpreter #2

Closed Keno closed 5 years ago

Keno commented 5 years ago

This all currently relies on manually adding special case foreigncall support for all commonly called functions: https://github.com/JuliaLang/julia/blob/kf/wasm3/src/interpreter.c#L547-L1038. It's gotten us this far, but it's a very ugly hack. We should be able to look up the wasm function pointers, compute the wasm signature from the julia signature and just immediately jump to it. The only thing I couldn't figure out here is how to get that function pointer given the name of the function. I tried something like:

    lookup_julia_fptr: function(name) {
        name = Pointer_stringify(name);
        return Module["asm"]['_' + name].name;
    }

but that doesn't work properly (i.e. the number I get back is not the correct function pointer). If somebody can figure out how to take care of that, the rest should be simple.

tshort commented 5 years ago

I don't think we can call into libjulia functions like _jl_cpu_threads unless they are exported. It looks like they are not exported in hello.wast. Maybe JL_DLLEXPORT could be defined to also include Emscripten's EMSCRIPTEN_KEEPALIVE. That should make them all exported. I think there's also a way to export everything.

Keno commented 5 years ago

Yes, I've done that locally (sorry, should have mentioned) and it does add it to the export list, but that still doesn't help me get their fptrs. The two things I've contemplated doing if there's no solution in the wasm JS API is to post process the .wasm file to build this map and store it as a JS dict or to just generate the C code I write now manually.

tshort commented 5 years ago

For exported functions, I think you can do getCFunc("jl_initialize") or Module["_jl_initialize"] on the JS side.

Keno commented 5 years ago

Right the problem is that I don't know how to get the wasm function id from that. It is possible to insert it into the function table but that incurs another round trip that should be avoidable, since a call.indirect with the correct function index should just work.

tshort commented 5 years ago

If you don't want to round-trip through JS, then I think you have to generate the C code like you mentioned. If you want to use getCFunc via JavaScript, I think that'll be something like the following in C:

extern int getCFunc(char *);

...

  fn = getCFunc(name);

On the JS side, getCFunc gets passed in with the environment. This is pretty easy with wasm-ffi. With just Emscripten, I think you can do the same thing.

Keno commented 5 years ago

If you don't want to round-trip through JS, then I think you have to generate the C code like you mentioned.

Well, it worked fine when I looked up the correct id manually in the binary. So either we need to automate that, which I feel like should be possible, I just don't know if it's exposed in javascript, or we need to generate C code.

Keno commented 5 years ago

Looks like what I may want to do is build a reverse dictionary from WebAssembly.Module.customSections(mod, "name");. Let me try that.

Keno commented 5 years ago

I've learned some things about how wasm works. In particular it looks like to call a function it should either be first declared in exports or in element (the latter of which initializes the function table we jump through). I think the next think I'm gonna try is to parse the c source code to automatically generate the stuff I wrote manually. That shouldn't be too hard, and be both robust and fast. In the future, esp once we figure out jit and dynamic libraries, we can revisit this.

Keno commented 5 years ago

Leaving for myself an earlier attempt, in case I want to go back to it:

diff --git a/website/repl.js b/website/repl.js
index 566ace2..2647dac 100644
--- a/website/repl.js
+++ b/website/repl.js
@@ -671,6 +671,48 @@ function process_input(input) {
     enable_prompt();
 }

+function decode_leb128(buffer, idx) {
+    var result = 0;
+    var shift = 0;
+    while (true) {
+        if (shift > (32 - 7)) {
+            throw("Out of range");
+        }
+        b = buffer[idx]; idx += 1;
+        result |= (b & 0x7f) << shift;
+        if ((b & 0x80) == 0) {
+            return [result, idx];
+        }
+        shift += 7;
+    }
+}
+
+const NAME_TYPE_FUNCITON = 1;
+function decode_name_section(sect) {
+    idx = 0;
+    var function_mapping = {};
+    while (idx < sect.length) {
+        [name_type, idx] = decode_leb128(sect, idx);
+        [subsection_size, idx] = decode_leb128(sect, idx);
+        if (name_type == NAME_TYPE_FUNCITON) {
+            [count, idx] = decode_leb128(sect, idx);
+            n = 0;
+            while (n < count) {
+                [index, idx] = decode_leb128(sect, idx);
+                [name_len, idx] = decode_leb128(sect, idx);
+                name = UTF8Decoder.decode(sect.subarray(idx, idx+name_len));
+                function_mapping[name] = index;
+                idx += name_len;
+                n += 1;
+            }
+            return function_mapping;
+        } else {
+            idx += subsection_size;
+        }
+    }
+    throw("Name section not present");
+}
+
 var Module = {
   preRun: [],
   postRun: [],
@@ -703,7 +745,23 @@ var Module = {
     Module.stringToUTF8(input, ptr, input.length + 1);
     Module._jl_eval_string(ptr);
     enable_prompt();
-  }]
+  }],
+  instantiateWasm: function(info, receiveInstance) {
+    WebAssembly.instantiateStreaming(fetch("hello.wasm", { credentials: 'same-origin' }), info)
+        .then(function(output) {
+            Module.wasmModule = output.module;
+            Module.fmap = decode_name_section(new Uint8Array(WebAssembly.Module.customSections(Module.wasmModule, "name")[0]));
+            receiveInstance(output.instance)
+        })
+        .catch(function(reason) {
+          // We expect the most common failure cause to be a bad MIME type for the binary,
+          // in which case falling back to ArrayBuffer instantiation should work.
+          err('wasm streaming compile failed: ' + reason);
+          err('falling back to ArrayBuffer instantiation');
+          instantiateArrayBuffer(receiveInstantiatedSource);
+        });
+    return {}; // Compiling asynchronously, no exports.
+  }
 };
 Module.setStatus('Downloading...');
 window.onerror = function(event) {