0

Call webassembly with import statement from embedded v8 ( without JS )

Following the thread Call webassembly from embedded v8 without JS I was able to call a WebAssembly code directly from c++. My problem started when I tried to run a more "complex" code ( see attached code ) that includes an import statement. When trying to run this code I get a v8 error WebAssembly.Instance(): Imports argument must be present and must be an object. I dag into the v8 code and found that this error happens when the module's import_table is empty ( v8/src/wasm/module-instantiate.cc#276 ). I think I need to provide the implementation of the imported function but I couldn't figure out how to do it.

#include <include/v8.h>

#include <include/libplatform/libplatform.h>

using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Promise;
using v8::WasmModuleObjectBuilderStreaming;
using v8::WasmCompiledModule;
using v8::Context;
using v8::Local;
using v8::Value;
using v8::String;
using v8::Object;
using v8::Function;
using v8::Int32;
using args_type = Local<Value>[];

int main(int argc, char* argv[]) {
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  Isolate::CreateParams create_params;
  create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  Isolate* isolate = Isolate::New(create_params);
  Isolate::Scope isolate_scope(isolate);
  HandleScope scope(isolate);
  Local<Context> context = Context::New(isolate);
  Context::Scope context_scope(context);

  WasmModuleObjectBuilderStreaming stream(isolate);

    // Use the v8 API to generate a WebAssembly module.
    //  compiled from the following c code 
    //  
    //  #include <stdlib.h>
    //  int add(int x, int y) {
    //      return x + rand();
    //  }
    //  
    //  produce the following wasm code 
    //  
    //  (module
    //  (type $FUNCSIG$i(func(result i32)))
    //      (import "env" "rand" (func $rand(result i32)))
    //      (table 0 anyfunc)
    //      (memory $0 1)
    //      (export "memory" (memory $0))
    //      (export "add" (func $add))
    //      (func $add(; 1;) (param $0 i32) (param $1 i32) (result i32)
    //      (i32.add
    //      (call $rand)
    //          (get_local $0)
    //          )
    //          )
    //      )
    // 
    // binary representation of the above code 
    std::vector<uint8_t> wasmbin{
            0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x8b ,0x80 ,0x80 ,0x80 ,0x00 ,0x02 ,0x60 ,0x00 ,0x01 ,
            0x7f ,0x60 ,0x02 ,0x7f ,0x7f ,0x01 ,0x7f ,0x02 ,0x8c ,0x80 ,0x80 ,0x80 ,0x00 ,0x01 ,0x03 ,0x65 ,0x6e ,0x76 ,
            0x04 ,0x72 ,0x61 ,0x6e ,0x64 ,0x00 ,0x00 ,0x03 ,0x82 ,0x80 ,0x80 ,0x80 ,0x00 ,0x01 ,0x01 ,0x04 ,0x84 ,0x80 ,
            0x80 ,0x80 ,0x00 ,0x01 ,0x70 ,0x00 ,0x00 ,0x05 ,0x83 ,0x80 ,0x80 ,0x80 ,0x00 ,0x01 ,0x00 ,0x01 ,0x06 ,0x81 ,
            0x80 ,0x80 ,0x80 ,0x00 ,0x00 ,0x07 ,0x90 ,0x80 ,0x80 ,0x80 ,0x00 ,0x02 ,0x06 ,0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,
            0x79 ,0x02 ,0x00 ,0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x01 ,0x0a ,0x8d ,0x80 ,0x80 ,0x80 ,0x00 ,0x01 ,0x87 ,0x80 ,
            0x80 ,0x80 ,0x00 ,0x00 ,0x10 ,0x00 ,0x20 ,0x00 ,0x6a ,0x0b
    };

  // same as calling:
  // let module = new WebAssembly.Module(bytes);
  Local<WasmCompiledModule> module = WasmCompiledModule::DeserializeOrCompile(isolate,
      WasmCompiledModule::BufferReference(0, 0),
      WasmCompiledModule::BufferReference(wasmbin.data(), wasmbin.size())
      ).ToLocalChecked();

  // same as calling:
  // let module_instance_exports = new WebAssembly.Instance(module).exports;
  args_type instance_args{module};
  Local<Object> module_instance_exports = context->Global()
    ->Get(context, String::NewFromUtf8(isolate, "WebAssembly"))
    .ToLocalChecked().As<Object>()
    ->Get(context, String::NewFromUtf8(isolate, "Instance"))
    .ToLocalChecked().As<Object>()
    ->CallAsConstructor(context, 1, instance_args)
    .ToLocalChecked().As<Object>()
    ->Get(context, String::NewFromUtf8(isolate, "exports"))
    .ToLocalChecked().As<Object>()
    ;

  // same as calling:
  // module_instance_exports.add(77, 0)
  args_type add_args{Int32::New(isolate, 77), Int32::New(isolate, 0)};
  Local<Int32> adder_res = module_instance_exports
    ->Get(context, String::NewFromUtf8(isolate, "add"))
    .ToLocalChecked().As<Function>()
    ->Call(context, context->Global(), 2, add_args)
    .ToLocalChecked().As<Int32>();

  printf("77 + rand() = %d\n", adder_res->Value());
  return 0;
}

Any help will be welcomed.

Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70
Yakir E
  • 84
  • 4

1 Answers1

0

I would recommend to use the WebAssembly C/C++ API, which V8 implements in the form of a library called "libwee8". The documentation for that is currently here. That should be the easiest way to run WebAssembly modules from C++ without involving JavaScript. As an added bonus, it makes it easy to switch the underlying implementation, if for whatever reason in the future life of your product V8 won't be the right solution for a given use-case.

That said, if you prefer to do everything by yourself using the (JavaScript-focused!) traditional V8 API, then you can look at the prototype implementation that maps the Wasm C++ API onto the (slightly modified, see other files in that directory) V8 API here: https://github.com/WebAssembly/wasm-c-api/blob/master/src/wasm-v8.cc. Be aware that due to the indirections required, this will be quite a bit slower than using libwee8, but if you're reinventing the wheel for educational purposes, that might not matter to you ;-)

Specifically, in https://github.com/WebAssembly/wasm-c-api/blob/master/src/wasm-v8.cc#L2106 you can see how the instantiation call takes an imports object as its second argument. As the error message you got informs you, it must be present (and it must be a JavaScript object), even when it is empty.

jmrk
  • 34,271
  • 7
  • 59
  • 74
  • thanks for the quick and elaborate reply. I followed your advice and figure out that I need to pass a second argument to *args_type instance_args{module,* **imports** *};* that will include the import table. I've tried to figure out how to create this import object and populate it with host function ( in my case the host *rand()* function but without any help. Reading in [link](https://github.com/WebAssembly/wasm-c-api) I've noticed that this might not even being supported by V8 ( for now ). Any idea if it's indeed not supported or am I missing something. 10x – Yakir E Nov 04 '19 at 15:24
  • It is supported, just convoluted and fairly inefficient: `v8::Object::New` creates a new object, and then you'll have to create a JavaScript-compatible function that wraps the host function you want to expose, and install that as a property on the object. I don't know of a good example you could follow, but that's the general concept. The Wasm C API makes all this much easier. – jmrk Nov 04 '19 at 19:52
  • I see. The thing is that our product can run the same JS code in browser and on tablets. We have some complected algorithm that we need to run and we wanted to share the same webasm code in the browser and in the embedded app so, as I see it, I have no choice but to use the v8 API. From your comment I understand now what I need to do. I will do it and post a working example when I'll get it running. Thanks for your help! – Yakir E Nov 06 '19 at 06:23