Skip to content
This repository has been archived by the owner on Aug 24, 2022. It is now read-only.

PInvoke in the browser via Emscripten

Katelyn Gadd edited this page Jul 5, 2015 · 5 revisions

Compiling your native libraries with emscripten allows you to use them via PInvoke, just like on the Microsoft and Mono .NET runtimes. This is still experimental, so please file reports about any issues you encounter.

There is a minimal sample program with working PInvoke for reference purposes.

Compiling C/C++ code with emcc

See the emscripten documentation. For exposing functions to JSIL, you'll want to mark them with EMSCRIPTEN_KEEPALIVE. C++ example:

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#define export(T) extern "C" T EMSCRIPTEN_KEEPALIVE 
#else
#define export(T) extern "C" __declspec(dllexport) T
#endif

export(int) Add(int a, int b) {
    return a + b;
}

There are emcc compiler options that you'll want to provide, so a basic command line will look like this:

emcc -s MODULARIZE=1 -s EXPORT_FUNCTION_TABLES=1 clib.cpp -o clib.js
  • -s MODULARIZE=1 ensures that multiple emscripten modules can be loaded within a single page. JSIL uses this to properly handle scenarios where you are using multiple different DLLs.
  • -s EXPORT_FUNCTION_TABLES=1 makes all the functions in your module available from outside. JSIL uses this to locate them and call them directly without having to go through emscripten's (very slow, limited) foreign call interface (ccall, cwrap, etc.)

You may also want to use the following option(s):

  • -s RESERVED_FUNCTION_POINTERS=n reserves a fixed-size table inside your emscripten module for function pointers. Doing this allows you to allocate native function pointers from managed C# delegates, for callbacks and the like. Setting this number too low (or not setting it at all) will cause any code manipulating native function pointers to fail.

Loading your emscripten-compiled DLL at runtime

For a native dll called clib, you would create a manifest file clib.dll.manifest.js containing the following:

if (typeof (contentManifest) !== "object") { JSIL.GlobalNamespace.contentManifest = {}; };

contentManifest["clib.dll"] = [
    ["NativeLibrary", "../../MinimalPInvoke/clib.js"]
];

You will also want to add an entry to jsilConfig telling it where to find the manifest, e.g.:

jsilConfig.manifests = [
    "MinimalPInvoke.exe",
    "../../clib.dll"
]

Note that the NativeLibrary path is relative to the libraryRoot specified in jsilConfig, while the manifest path is relative to manifestRoot.

Limitations

Structure layout and type size behavior are roughly compatible with .NET, but certain data layouts are not easy to represent in JavaScript. Be aware that in these corner cases, the PInvoke layer may not be able to accurately guess emscripten's memory layout for your types. Please file issue reports when you hit a problem of this sort.

Some less-commonly-used PInvoke features are only partially implemented or not implemented at all. You can browse the Emscripten Test Suite to see which PInvoke features are expected to work. Either way, try your application out, and file an issue if it doesn't work - we can probably fix it!

Emscripten has an entirely separate heap and memory management system. This means that you cannot meaningfully pass an IntPtr via PInvoke if it refers to memory allocated by .NET code - it will be a random garbage address as far as emscripten is concerned. Using strong types (MyStruct, ref MyStruct, MyStruct *, MyStruct[]) gives the PInvoke layer enough information to make sure data is available to emscripten.

By default no data is allocated inside the emscripten heap. This means the PInvoke layer has to copy all parameters into the emscripten heap before each call, and then any ref/out parameters have to be copied back into the managed heap.

Any data not allocated in the emscripten heap will expire once a PInvoke call completes. On a typical native platform, pinning data (with fixed or GCHandle.Alloc) would be sufficient to keep it accessible to native code, but the same is not true for emscripten. If emscripten code needs to continue using managed data after a call, you should allocate the managed data using NativePackedArray (below) in order to ensure it has sufficient lifetime and is not destroyed by PInvoke cleanup.

NativePackedArray

To eliminate the need for PInvoke to copy data, you can allocate an array inside the emscripten heap using the JSIL.Runtime.NativePackedArray type. On the native CLR this type has no special behavior, but in JavaScript it will be a packed array with its storage allocated in a specific emscripten heap.

NativePackedArray's constructor takes two arguments: the name of the DLL and the length of the array. You must specify the DLL name in scenarios where you have multiple DLLs loaded, otherwise the allocation will occur in the wrong native heap and your data will end up being copied. Because it exists in the native heap, you will need to free it manually, using the IDisposable.Dispose method.

An example of NativePackedArray usage is below:

[DllImport("common.dll", CallingConvention=CallingConvention.Cdecl)]
public static extern int WriteStringIntoBuffer (
    byte * dest, int capacity
);

public static void Main () {
    using (var na = new NativePackedArray<byte>("common.dll", 512)) {
        int numBytes;
        fixed (byte* pBuffer = na.Array)
            numBytes = WriteStringIntoBuffer(pBuffer, na.Length);
    }
}

In this example, "common.dll" is used in both places, ensuring the allocation occurs in the correct heap. na.Array is a 512-element byte array that you can pin (via fixed or GCHandle.Alloc) as expected. Because the data lies in the native heap, it produces native pointers when pinned, and the PInvoke layer is able to avoid copying the data. The using block ensures that the allocation is freed once it is no longer needed.