Derek Thurn
Derek Thurn

Reputation: 15365

Returning a byte array from JavaScript to Emscripten/Unity Web Assembly

I have some JavaScript data which I am serializing so that Unity can consume it as a byte[] array, as generally described here. I want to expose this data to Unity via a jslib interface like this:

[DllImport("__Internal")]
private static extern byte[] GetByteArray();

I attempted to bridge my values from JavaScript using the built-in methods in emscripten:

  GetByteArray: function () {
    var myTypedArray = new Uint8Array([10, 20, 30, 40, 50]);
    var buf = _malloc(myTypedArray.length * myTypedArray.BYTES_PER_ELEMENT);
    writeArrayToMemory(myTypedArray, buf);
    return buf;
  },

Unfortunately, this does not work -- the webassembly C# just sees a 0-length array as the return type. I also tried stuff like HEAPU8.set(myTypedArray, buf).

Questions:

  1. What is the correct way to do this?
  2. Failing that, am I at least doing this correctly from a JavaScript-to-C perspective? Is the problem more likely with how C# represents byte arrays?

Upvotes: 2

Views: 1856

Answers (3)

RipperDoc
RipperDoc

Reputation: 704

One approach that I've used when I want as little trouble as possible is to save the data as a file in the emscripten in-memory file system and then read it back on C# side. Haven't measure the performance impact but because it's in-memory in theory it's quite performant.

Something like this:

JSLib

FS.writeFile('/tmp/myfile', data);

Note that FS is a global variable only reachable from within the JSLib file (which ends up i webgl.framework.js when built in Unity). Whether it's in-memory filesystem depends on the path, see details here

C#

var data = File.ReadAllBytes("/tmp/myfile");

Upvotes: 0

savram
savram

Reputation: 580

The secret is to have a method to instantiate the byte array and call it from the js context. The method that instantiates the array then calls the js method that actually fills the byte array. It's a 2 step process.

C# side

[DllImport("__Internal", EntryPoint = "getByteArrayTmp")]
private static extern void getByteArrayTmp(Action<int> instantiateByteArrayCallback);
[DllImport("__Internal", EntryPoint = "getByteArray")]
private static extern void getByteArray(Action callback, byte[] byteArray);
 
private static event Action callbackEvent;
 
public static byte[] byteArray;
 
[MonoPInvokeCallback(typeof(Action))]
private static void callback() {
    callbackEvent?.Invoke();
    callbackEvent = null;
}
 
[MonoPInvokeCallback(typeof(Action<int>))]
private static void instantiateByteArrayCallback(int size) {
    byteArray = new byte[size];
    getByteArray(callback, byteArray);
}

The method that is actually called by the user is

public static void getByteArray(Action callback) {
    #if UNITY_WEBGL && !UNITY_EDITOR
        callbackEvent = null;
        callbackEvent += callback;
        getByteArrayTmp(instantiateByteArrayCallback);
    #endif
}

On the js side of things

getByteArrayTmp: function(instantiateByteArrayCallback) {
    const byteArray = new Uint8Array([21, 31]);//example
    Module["yourplugin"].byteArray = byteArray;
    Module.dynCall_vi(instantiateByteArrayCallback, byteArray.length);
     
},
getByteArray: function(callback, byteArray) {
    Module.HEAPU8.set(Module["yourplugin"].byteArray, byteArray);
    Module.dynCall_v(callback);
},

This is several times faster than using a string as an intermediate. You can transfer over 20MB from js to Unity in less than a second like this, while using the string method it takes several seconds.

Upvotes: 0

Antoine Thiry
Antoine Thiry

Reputation: 2442

I hope you found a solution to your issue. Here's my (really hacky solution) but it works :

jslib

GetByteArray: function(){
    var array = new Uint8Array([10, 20, 30, 40, 50]);
    var str = array.toString();
    str = '[' + str + ']';
    unityInstance.sendMessage('gameObject', 'ByteArrayAvailable', str);
}

c#

#if UNITY_WEBGL && !UNITY_EDITOR

[DllImport("__Internal")]
private static extern void GetByteArray();

#endif

...
    
void CallWebGL()
{
    GetByteArray();
}

void ByteArrayAvailable(string byteArrayStr)
{
    var byteArray = JsonConvert.DeserializeObject<byte[]>(byteArrayStr);
}

You need to add Newtonsoft.Json.dll to you plugins folder to make it work but that's the only easy way I found to make it work :)

Upvotes: 1

Related Questions