Working with Wasm Using JSVM-API

Introduction

JSVM-API provides APIs for compiling the WebAssembly (Wasm) bytecode, optimizing Wasm functions, and serializing and deserializing Wasm caches.

Basic Concepts

  • Wasm module: a binary format that contains compiled Wasm code. You can use OH_JSVM_CompileWasmModule to create a Wasm module from Wasm bytecode or Wasm cache, and use OH_JSVM_IsWasmModuleObject to check whether a JSVM_Value is a Wasm module.
  • Wasm function: a function defined in a Wasm module. The functions in a Wasm module can be used by external code after being imported. You can use OH_JSVM_CompileWasmFunction to convert Wasm bytecode into the format that JSVM can execute efficiently.
  • Wasm cache: data generated by serializing the bytecode in a Wasm module. The cache holds the compiled Wasm code so that it can be reused, eliminating the need for recompiling the code. You can use OH_JSVM_CreateWasmCache (with cacheType set to JSVM_CACHE_TYPE_WASM) to create a Wasm cache instance and use OH_JSVM_ReleaseCache to release it.

Available APIs

API Description
OH_JSVM_CompileWasmModule Compiles the Wasm bytecode into a Wasm module. If the cache parameter is passed in, the cache will be deserialized into a Wasm module first. The compilation is performed when the deserialization fails.
OH_JSVM_CompileWasmFunction Compiles the function with the specified ID in a Wasm module into the optimized machine code. Currently, only the highest optimization level is enabled. The validity of the function ID is ensured by the caller.
OH_JSVM_IsWasmModuleObject Checks whether the input value is a Wasm module.
OH_JSVM_CreateWasmCache Serializes the machine code in a Wasm module into a Wasm cache. If the Wasm module does not contain machine code, the serialization will fail.
OH_JSVM_ReleaseCache Releases a Wasm cache instance created by JSVM-API. The cacheType and cacheData passed in must match. Otherwise, undefined behavior may occur.

Example

If you are just starting out with JSVM-API, see JSVM-API Development Process. The following demonstrates only the C++ code involved in the APIs for Wasm.

CPP code:

// hello.cpp
#include "napi/native_api.h"
#include "ark_runtime/jsvm.h"
#include <hilog/log.h>

#ifndef CHECK
#define CHECK(cond)                                  \
    do {                                             \
        if (!(cond)) {                               \
            OH_LOG_ERROR(LOG_APP, "CHECK FAILED");   \
            abort();                                 \
        }                                            \
    } while (0)
#endif

// Check whether a JSVM_Value is a Wasm module.
static bool IsWasmModuleObject(JSVM_Env env, JSVM_Value value) {
    bool result;
    JSVM_Status status = OH_JSVM_IsWasmModuleObject(env, value, &result);
    CHECK(status == JSVM_OK);
    return result;
}

// Create a JSVM string from a C string.
static JSVM_Value CreateString(JSVM_Env env, const char *str) {
    JSVM_Value jsvmStr;
    JSVM_Status status = OH_JSVM_CreateStringUtf8(env, str, JSVM_AUTO_LENGTH, &jsvmStr);
    CHECK(status == JSVM_OK);
    return jsvmStr;
}

// Create a JSVM number from a C int32_t value.
static JSVM_Value CreateInt32(JSVM_Env env, int32_t val) {
    JSVM_Value jsvmInt32;
    JSVM_Status status = OH_JSVM_CreateInt32(env, val, &jsvmInt32);
    CHECK(status == JSVM_OK);
    return jsvmInt32;
}

// Instantiate the Wasm module.
static JSVM_Value InstantiateWasmModule(JSVM_Env env, JSVM_Value wasmModule) {
    JSVM_Status status = JSVM_OK;
    JSVM_Value globalThis;
    status = OH_JSVM_GetGlobal(env, &globalThis);
    CHECK(status == JSVM_OK);

    JSVM_Value webAssembly;
    status = OH_JSVM_GetProperty(env, globalThis, CreateString(env, "WebAssembly"), &webAssembly);
    CHECK(status == JSVM_OK);

    JSVM_Value webAssemblyInstance;
    status = OH_JSVM_GetProperty(env, webAssembly, CreateString(env, "Instance"), &webAssemblyInstance);
    CHECK(status == JSVM_OK);

    JSVM_Value instance;
    JSVM_Value argv[] = {wasmModule};
    status = OH_JSVM_NewInstance(env, WebAssemblyInstance, 1, argv, &instance);
    CHECK(status == JSVM_OK);
    return instance;
}

// Obtain the Wasm bytecode (add module).
static std::vector<uint8_t> GetAddWasmBuffer() {
    // The following is the text format of the Wasm bytecode corresponding to wasmBuffer, which contains only the add function.
    // (module
    //   (func $add (param $lhs i32) (param $rhs i32) (result i32)
    //     local.get $lhs
    //     local.get $rhs
    //     i32.add
    //   )
    //   (export "add" (func $add))
    // )
    std::vector<uint8_t> wasmBuffer = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01,
                                       0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07,
                                       0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01,
                                       0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b};
    return wasmBuffer;
}

// Verify the Wasm instance function (add module).
static void VerifyAddWasmInstance(JSVM_Env env, JSVM_Value wasmInstance) {
    JSVM_Status status = JSVM_OK;
    // Obtain the exports.add function from the Wasm instance.
    JSVM_Value exports;
    status = OH_JSVM_GetProperty(env, wasmInstance, CreateString(env, "exports"), &exports);
    CHECK(status == JSVM_OK);

    JSVM_Value add;
    status = OH_JSVM_GetProperty(env, exports, CreateString(env, "add"), &add);
    CHECK(status == JSVM_OK);

    // Run the exports.add(1, 2). The expected result is 3.
    JSVM_Value undefined;
    OH_JSVM_GetUndefined(env, &undefined);
    JSVM_Value one = CreateInt32(env, 1);
    JSVM_Value two = CreateInt32(env, 2);
    JSVM_Value argv[] = {one, two};
    JSVM_Value result;
    status = OH_JSVM_CallFunction(env, undefined, add, 2, argv, &result);
    CHECK(status == JSVM_OK);
    int32_t resultInt32;
    OH_JSVM_GetValueInt32(env, result, &resultInt32);
    CHECK(resultInt32 == 3);
}

// Wasm demo main function.
static JSVM_Value WasmDemo(JSVM_Env env, JSVM_CallbackInfo info) {
    JSVM_Status status = JSVM_OK;
    std::vector<uint8_t> wasmBuffer = GetAddWasmBuffer();
    uint8_t *wasmBytecode = wasmBuffer.data();
    size_t wasmBytecodeLength = wasmBuffer.size();
    JSVM_Value wasmModule;
    // Obtain the Wasm module based on the Wasm bytecode.
    status = OH_JSVM_CompileWasmModule(env, wasmBytecode, wasmBytecodeLength, NULL, 0, NULL, &wasmModule);
    CHECK(status == JSVM_OK);
    CHECK(IsWasmModuleObject(env, wasmModule));

    // Perform compilation optimization on the first function (add) defined in the Wasm module.
    int32_t functionIndex = 0;
    // Currently, only high-level optimization is supported. That is, the effect is the same no matter whether JSVM_WASM_OPT_BASELINE or JSVM_WASM_OPT_HIGH is passed in.
    status = OH_JSVM_CompileWasmFunction(env, wasmModule, functionIndex, JSVM_WASM_OPT_HIGH);
    CHECK(status == JSVM_OK);
    // Instantiate the compiled Wasm module.
    JSVM_Value wasmInstance = InstantiateWasmModule(env, wasmModule);
    // Verify the function in the instantiated Wasm instance.
    VerifyAddWasmInstance(env, wasmInstance);

    // Create a Wasm cache.
    const uint8_t *wasmCacheData = NULL;
    size_t wasmCacheLength = 0;
    status = OH_JSVM_CreateWasmCache(env, wasmModule, &wasmCacheData, &wasmCacheLength);
    CHECK(status == JSVM_OK);
    // The Wasm cache is created successfully.
    CHECK(wasmCacheData != NULL);
    CHECK(wasmCacheLength > 0);

    // Assign a value to the Wasm cache to simulate cache persistence. In actual scenarios, the Wasm cache may be saved to a file.
    std::vector<uint8_t> cacheBuffer(wasmCacheData, wasmCacheData + wasmCacheLength);

    // Once the cache is saved, it needs to be released explicitly to avoid memory leaks.
    // Note that the input JSVM_CacheType must match the cache data.
    status = OH_JSVM_ReleaseCache(env, wasmCacheData, JSVM_CACHE_TYPE_WASM);
    CHECK(status == JSVM_OK);

    // Deserialize the Wasm code to generate a Wasm module.
    bool cacheRejected;
    JSVM_Value wasmModule2;
    status = OH_JSVM_CompileWasmModule(env, wasmBytecode, wasmBytecodeLength, cacheBuffer.data(), cacheBuffer.size(),
                                       &cacheRejected, &wasmModule2);
    CHECK(status == JSVM_OK);
    // If the input Wasm cache is matched and the internal verification (such as the version) is successful, the cache will be accepted.
    CHECK(cacheRejected == false);
    CHECK(IsWasmModuleObject(env, wasmModule2));

    // For wasmModule2 (obtained through deserialization), perform the same operations, including function compilation, instantiation, and verification.
    status = OH_JSVM_CompileWasmFunction(env, wasmModule2, functionIndex, JSVM_WASM_OPT_HIGH);
    CHECK(status == JSVM_OK);
    JSVM_Value wasmInstance2 = InstantiateWasmModule(env, wasmModule);
    VerifyAddWasmInstance(env, wasmInstance2);

    JSVM_Value result;
    OH_JSVM_GetBoolean(env, true, &result);
    return result;
}

// Register a WasmDemo callback.
static JSVM_CallbackStruct param[] = {
    {.data = nullptr, .callback = WasmDemo}
};
static JSVM_CallbackStruct *method = param;
// Register the C++ WasmDemo callback as a JSVM globalThis.wasmDemo property for the JS to call.
static JSVM_PropertyDescriptor descriptor[] = {
    {"wasmDemo", nullptr, method++, nullptr, nullptr, nullptr, JSVM_DEFAULT},
};

// Call the C++ callback from JS.
const char *srcCallNative = R"JS(wasmDemo())JS";