@brendanduncan_u3d Is it possible to send a byte array from jslib to Unity? I saw this SO post, but I would like to skip the conversion step if possible. It takes more time to convert from a string to a byte array. It can even be a shared byte array from the heap. For example, I saw this forum post with a shared float array that was declared in Unity, but the problem is that I don’t know the size of the array at the time of declaration. I want to malloc this array in the jslib and use it in Unity directly, without the conversion step from the SO post. Is it possible?
_malloc allocates memory on the shared WASM heap. If you have JS specific memory, like a string, you do need to copy it to the shared WASM heap to be accessible from C#/C++. There’s the function stringToUTF8 to convert a string to byte array.
This is not what I meant. I found a solution to my issue, and what I’m doing is many, many times faster than the answer on SO. I can transfer byte arrays over 20MB from the js context to Unity context in less than a second instead of taking several seconds now. I’ll post my solution here in case anyone wants to do this as well.
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, because if you try to do it with one function like the SO post says, it doesn’t work, and you have to use a string as an intermediary form(which is very slow).
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);
},
@brendanduncan_u3d if you know of an easier solution please let me know, but this works already :).
Hei @Marks4 which version of Unity are you using? As as I know, now it’s not necessary to type Module in order to call dynCall method.
This is the code I’ve been using to send the UA data to Unity.
sendData:function(callback, data) {
const buffer = _malloc(data.length * data.BYTES_PER_ELEMENT);
HEAPU8.set(data, buffer);
dynCall('vii', callback, [buffer, data.length]);
_free(buffer);
}
But I need to alloc memory and afterwards, delete the buffer.
Can you show me the C# side of it? What method on C# gets the buffer and data.length? Is it a public byte array?
I only type Module because I like to know where the methods come from, even though it’s unnecessary and works without it. It’s the same reason I use window.devicePixelRatio
and not just devicePixelRatio
for example.
Looking at it more closely, I’m not sure IL2CPP lets you get arbitrary byte[ ] data that was _malloc’d in JS. You can get a C# string from JS _malloc data, which IL2CPP takes care of, but I’m not seeing a way to get byte[ ] from JS. When IL2CPP generates the code to return a byte[ ], the array always has a length of 1. So, allocating the data from C# and passing the pointer to JS might be the best method for shared data. I’ll ask around to see if getting byte[ ] data from JS into C# is possible.
@brendanduncan_u3d That’s why my method takes 2 steps. First I create the array in js and save in the js context, but send the byte array length information to C#. Then on the C# side I instantiate the array and pass the pointer to JS, and finally I set the memory in JS.
What about @GroovyFox 's solution? He didn’t show the C# side, but it looks like he’s sending the byte array directly?
Hi @Marks4 ! sure this is the C# side.
First the method callback
[DllImport("__Internal")]
private static extern void init(Action<byte[], int> ReceiveArrayData);
[AOT.MonoPInvokeCallback(typeof(Action<byte[], int>))]
private static void OnByteArray(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 1)] byte[] data, int length)
{
if (data != null)
{
// process your data
}
}
Note hat the sendData from the Js library, sends an Uint8ClampedArray typed array (represents an array of 8-bit unsigned integers clamped to 0-255), so it needs to be marshalled as described above.
Also, when the init is called, it passes the ReceiveArrayData arg with OnByteArray
public void Initialize()
{
init(OnByteArray);
}
Finally in the Js lib, this is way I’m storing the callback for later use
$utils: {
onByteArray: {},
},
init: function (onByteArray) {
utils.onByteArray = onByteArray;
},
Amazing, thank you for sharing your method. It’s still a 2 step process like mine, but it has the advantage(or disadvantage depending on the use case) of not requiring a public byte array!
@GroovyFox Just one questions regarding the length parameter. Why do you send the length to C#? If you can marshal the byte array, you can simply get the byte array’s length in C# with “mybytearray.lengh”, no?
Yep, it’s a 2 step process. And you’re right @Marks4 , it can be accessed with the array length, but i like to pass the length from the js lib.
@GroovyFox @brendanduncan_u3d I just noticed one thing that I never used before. This utils. What's the purpose of the ""? It makes a scoped variable in that jslib? Or can $utils be seen from other jslibs as well? So you use it like this?
mergeInto(LibraryManager.library, {
$utils: {
onByteArray: {},
},
init: function (onByteArray) {
utils.onByteArray = onByteArray;
},
});
Normally I’d use a .jspre file for the utils. I didn’t know you could do it in the jslib.
@Marks4 that’s another interesting question, I’m using it to encapsulate member like variables to be called in this scope.
But actually I haven’t found solid documentation to explain the use of the “$” (not even in the emscripten site) and I’ve seen it a lot in jslibs.
Not sure if it’s exposed to the other libs as well, there’s a way to find out.
Cheers
This $ variable might be a Unity thing only. @brendanduncan_u3d Can you shed some light on the matter? Thanks!
The is an Emscripten thing, and like many Emscripten things, poorly documented. It is mentioned in passing in https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html, at the bottom of the **JavaScript limits in library files** section, in the Note block, where it says "Keys starting with have the $ stripped and no underscore added."
In the words of the great Douglas Adams:
It was on display in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying ‘Beware of the Leopard.’
@brendanduncan_u3d is this correct?
mergeInto(LibraryManager.library, {
$utils: {
onByteArray: {},
},
init: function (onByteArray) {
utils.onByteArray = onByteArray;
},
});
On Unity 2019, I get an error: utils is not defined. How is the correct way to use this $ sign?
The code stripping can only automatically detect usage from C. In this case, utils is not getting detected as being used, and is getting stripped as unused. You need to add it to the __deps of init.
init__deps: ["$utils"],
init: function...
That makes sense using __deps in that way.
I also used it the other way:
const myLib = {
$utils: {
onByteArray: {}
}
};
autoAddDeps(myLib, '$utils');
mergeInto(LibraryManager.library, myLib);
Thanks @GroovyFox , this is of great assistance !!
I’ll use this from now on instead of SendMessage because this is much more efficient and evitates any Serialization/Deserialization process.
For anyone coming after, here is a few tricks I discovered along the way:
- You can also efficiently pass to Unity some float[ ] or int[ ] this way using “HEAPF32.set”/“HEAP32.set”:
In JsLib:
const myLib = {
$utils: {
onArrayFloat: null,
},
$sendDataFloat: function(floatArray) {
const buffer = _malloc(floatArray.length * floatArray.BYTES_PER_ELEMENT);
HEAPF32.set(floatArray, buffer >> 2);
dynCall('vii', utils.onArrayFloat, [buffer, floatArray.length]);
_free(buffer);
},
init: function (onByteArrayFloat) {
utils.onArrayFloat= onArrayFloat;
window.unitySendDataFloat = sendDataFloat;
},
};
autoAddDeps(myLib, "$utils");
autoAddDeps(myLib, "$sendDataFloat");
mergeInto(LibraryManager.library, myLib);
In your Unity C# code:
public class PocArrays : MonoBehaviour
{
[DllImport("__Internal")]
public static extern void init(Action<float[], int> ReceiveFloatArrayData);
[AOT.MonoPInvokeCallback(typeof(Action<float[], int>))]
public static void OnFloatArray([MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.R4, SizeParamIndex = 1)] float[] floats, int floatsLength
)
{
if (floats != null )
{
foreach (var item in floats)
{
Debug.Log("float element is " + item);
}
}
}
void Awake()
{
init(OnFloatArray);
}
}
In your js code:
window.unitySendData(new Float32Array([755.188,18.255, 45.588]));
- You can pass as many arguments as you want using dyncall. When adding arguments, make sure you update the part with “dynCall(‘viiiiiiii’”. v means void (the return type) and all the “i” are the types of arguments. For example, when adding an int (which is a pointer to an array in our case), add an “i” at the end of the string. All of the supported types are available here: dyncall Manual - Bindings to programming languages
Also, make sure you add the proper marshaling types on the C# side:
[AOT.MonoPInvokeCallback(typeof(Action<float[], int, int[], int, byte[], int>))]
public static void OnFloatArray(
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.R4, SizeParamIndex = 1)] float[] myFloats, int floatsLength,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4, SizeParamIndex = 3)] int[] myInts, int intsLength,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 5)] byte[] myBytes, int bytesLength
)
{
// Directly use myFloats / myInts / myBytes
}
In JsLib
// Note the iiiiii (because there is 6 int (3 being pointers) as an argument)
dynCall('viiiiii', utils.onReceiveMyArrays, [aBuffer, a.length, bBuffer, b.length, cBuffer, c.length]);
In your Js
Passing float[ ], int[ ], string as byte[ ] to Unity in a single call.
javascript** **window.unitySendData(new Float32Array([1.188,1.256]), new Int32Array([155,1, 488]), new TextEncoder().encode("abcd"));** **
- To call the JsLib functions directly from javascript, I declared the function I want to use in the window scope, which is not especially good practice as it messing up the windows scope. As mentionned by @brendanduncan_u3d , to solve this, you may also declare an “exposedFunctions” object in the Module object which can be accessed from the JsLib scope and directly call it from the unityInstance object that you have stored in your html. In this “exposedFunctions” object you may put all of the functions you want to expose to javascript. It still requires to be initialized in the init function though.
In JsLib
...
$exposedFunctions: {
sendData: null,
},
init: function () {
Module.exposedFunctions = exposedFunctions;
exposedFunctions.sendData = sendData;
},
...
autoAddDeps(myLib, "$exposedFunctions");
...
In Js
unityInstance.Module.exposedFunctions.sendData(...)
@pohype The Module object is often used instead of window for putting things into a global scope. Keeps things a bit more organized.
@brendanduncan_u3d thank you! I also thought this but for some strange reason, i cannot find my jsLibs functions in the Module object. Are those supposed to be at the root of the Module object or somewhere else? Or is there some specific syntax to embed those as its not automatic ?