Skip to content

Feature Request: Support for jco transpilation / V8 runtime #103

Open

Description

@dirkwa

Feature Request: Support for jco transpilation / V8 runtime

Summary

Components built with componentize-dotnet cannot be executed in Node.js via jco transpilation. The component builds and transpiles successfully, but calling any exported function results in RuntimeError: null function or function signature mismatch.

Environment

  • componentize-dotnet: 0.7.0-preview00010
  • .NET SDK: 10.0.100-preview.7
  • jco: 1.10.0
  • Node.js: 22.x
  • OS: Windows 11 / Raspberry Pi OS (both tested)

Reproduction Steps

1. Create a minimal .NET WASI component

// PluginImpl.cs
using SignalkPluginWorld.wit.exports.signalk.plugin.v1_0_0;

namespace SignalkPluginWorld.wit.exports.signalk.plugin.v1_0_0;

public class PluginImpl : IPlugin
{
    public static string PluginId() => "test-plugin";
    public static string PluginName() => "Test Plugin";
    public static string PluginSchema() => "{}";
    public static int PluginStart(string config) => 0;
    public static int PluginStop() => 0;
}

2. Build the component

dotnet build
# Output: bin/Debug/net10.0/wasi-wasm/native/MyPlugin.wasm

3. Transpile with jco

npx @bytecodealliance/jco transpile MyPlugin.wasm -o jco-output --name mymodule --tla-compat

4. Try to use in Node.js

import * as module from './jco-output/mymodule.js';

await module.$init;
console.log('Init complete');

// This crashes:
const id = module.plugin.pluginId();

Expected Behavior

After $init completes, calling pluginId() should return the string "test-plugin".

Actual Behavior

RuntimeError: null function or function signature mismatch
    at pluginId (wasm://wasm/00a1b2c3:wasm-function[1234]:0x12345)
    at pluginId (file:///path/to/jco-output/mymodule.js:6220:28)

Technical Analysis

What the WASM exports

Using jco print on the compiled component, we can see:

(export "_initialize" (func $_initialize))
(export "InitializeModules" (func $...))
(export "signalk:plugin/plugin@1.0.0#plugin-id" (func $...))
(export "signalk:plugin/plugin@1.0.0#plugin-name" (func $...))
...
(table (;0;) 3983 3983 funcref)

The component:

  • Exports _initialize (WASI reactor initialization)
  • Exports InitializeModules (.NET runtime initialization)
  • Has a function table with 3983 entries
  • Exports the expected WIT functions

What jco generates

The generated JavaScript correctly:

  1. Instantiates the WASM module
  2. Sets _initialized = true after instantiation
  3. Assigns function references: plugin100PluginId = exports1['signalk:plugin/plugin@1.0.0#plugin-id']

Where it fails

When pluginId() is called, it invokes the WASM function which internally uses call_indirect to dispatch through the function table. In V8, the table entries are null, causing the crash.

Why it works in Wasmtime

In Wasmtime, the _initialize function properly populates the indirect call table. In V8 (via jco transpilation), this initialization either:

  1. Doesn't happen correctly
  2. Happens but the table isn't shared properly between the shim and core modules

Workarounds Attempted (None Successful)

  1. Manual _initialize() call - Added explicit call in jco output; no effect
  2. Manual InitializeModules() call - Crashes because _initialize already calls it
  3. Different jco flags - --instantiation sync, --tla-compat, etc.
  4. Removing [ThreadStatic] - Fixed build issues but not runtime

Questions for the Team

  1. Is jco/V8 support planned for componentize-dotnet?
  2. Is this a known limitation of the NativeAOT-LLVM approach?
  3. Are there any workarounds to make the indirect call table work in V8?
  4. Would a different compilation approach (e.g., Mono interpreter) work better with jco?

Use Case

We're building a plugin system for Signal K marine data server. The server runs on Node.js and we want to support WASM plugins in multiple languages. Currently:

  • AssemblyScript: Works perfectly
  • Rust: Works perfectly
  • C#/.NET: Builds but can't execute

Adding .NET support would greatly benefit the enterprise/marine industry developers who prefer C#.

Related Links

Complete Reproduction Repository

A complete working example demonstrating this issue is available at:
https://github.com/dirkwa/signalk-server/tree/WASM_WASIX/examples/wasm-plugins/anchor-watch-dotnet


Labels suggestion: enhancement, runtime-support, jco

Activity

  1. added a commit that references this issue on Dec 2, 2025
  2. dirkwa commented on Jan 6, 2026

    @dirkwa
    Author

    Update: Issue also affects wasmtime (not jco/V8 specific)

    After extensive testing, I discovered this is not a jco/V8-specific issue - the component also fails in wasmtime itself with the same root cause.

    Test Results

    Runtime Version Error
    jco + Node.js 1.15.4 + v22.21.0 null function or function signature mismatch
    jco + Node.js 1.15.4 + v24.12.0 null function or function signature mismatch
    wasmtime (Rust API) 23.0.2 wasm trap: uninitialized element
    wasmtime (Rust API) 39.0.1 wasm trap: uninitialized element
    wasmtime (Rust API) 40.0.0 wasm trap: uninitialized element

    Note: wasmtime 23.0.2 is the version documented as the target in componentize-dotnet's README.

    Wasmtime Error Details

    Error: error while executing at wasm backtrace:
        0:  0x8a936 - S_P_CoreLib_System_Runtime_EH__DispatchException
                        at /_/src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/ExceptionHandling.wasm.cs:68
        1:  0x54836 - S_P_CoreLib_System_Runtime_EH__RhpThrowEx
        2:  0x5b3f5 - S_P_CoreLib_Internal_Runtime_CompilerHelpers_ThrowHelpers__ThrowNullReferenceException
                        at /_/src/libraries/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/ThrowHelpers.cs:20
        3:  0x54dcd - S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeGlobalTablesForModule
                        at /_/src/coreclr/nativeaot/Common/src/Internal/Runtime/CompilerHelpers/StartupCodeHelpers.cs:128
        4:  0x98b57 - S_P_CoreLib_Internal_Runtime_CompilerHelpers_StartupCodeHelpers__InitializeModules
        5:   0x250e - InitializeRuntime()
        ...
    
    Caused by:
        wasm trap: uninitialized element
    

    Implications

    This suggests the bug is in componentize-dotnet / .NET NativeAOT WASM code generation, not in how jco transpiles or how V8 handles the component. The indirect call table elements are not being properly initialized in the generated WASM output.

    The component passes wasm-tools validate, so it's structurally valid - the issue is with the runtime initialization code generated by NativeAOT.

    Test Harness

    I created a Rust test harness using the wasmtime crate to verify this. The harness uses wasmtime::component::bindgen! to generate bindings from the WIT file, then instantiates and calls the component - same error occurs.

    Repository with full reproduction: https://github.com/dirkwa/jco-issue-1173

    This may need to be escalated to dotnet/runtime since the issue appears to be in the NativeAOT-LLVM WASI-WASM code generation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions