Return a float/double array from jslib to Unity

I don’t want to share an array between jslib and Unity. What I want is to create an array in the js side and pass it as an argument from a function in Unity. I know how to do it with single values like string, int, float, using cwrap on the js side and MonoPInvokeCallback on the Unity side. But not arrays.

I tried following this, my c# function is a simple

public void testDouble(double[] args) {
        Debug.Log(args[0] + " " + args[1] + " " + args[2]);
    }

it logs garbage, not the values I set on the js side. Trying to use a Float64Array, if that makes any difference.

3 patterns.

using AOT;
using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class floatArrayTest : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern IntPtr JS_FloatArrayTest();

    [DllImport("__Internal")]
    private static extern IntPtr JS_FloatArrayTest2(Action<int, IntPtr> callback);

    [DllImport("__Internal")]
    private static extern IntPtr JS_FloatArrayTest3();

    [MonoPInvokeCallback(typeof(Action<int, IntPtr>))]
    private static void callback(int len2, IntPtr ptr2)
    {
        var arr2 = new float[len2];
        Marshal.Copy(ptr2, arr2, 0, len2);
        Debug.Log($"JS_FloatArrayTest2 len:{len2} > {arr2[0]}, {arr2[1]}, {arr2[2]}");
    }

    void Start()
    {
        var ptr = JS_FloatArrayTest();
        var len = Marshal.ReadInt32(ptr);
        var arr = new float[len];
        Marshal.Copy(new IntPtr(ptr.ToInt32() + 4), arr, 0, len);
        Debug.Log($"JS_FloatArrayTest len:{len} > {arr[0]}, {arr[1]}, {arr[2]}");

        JS_FloatArrayTest2(callback);

        var ptr3 = JS_FloatArrayTest3();
        var arr3 = new float[3];
        Marshal.Copy(ptr3, arr3, 0, 3);
        Debug.Log($"JS_FloatArrayTest3 len:{3} > {arr3[0]}, {arr3[1]}, {arr3[2]}");
    }
}
mergeInto(LibraryManager.library, {
    JS_FloatArrayTest: function() {
        var floatArray = [0.12, 3.45, 6.789];
        var ptr = _malloc((floatArray.length + 1) * 4); // 4 = BYTES_PER_ELEMENT
        HEAP32.set([floatArray.length], ptr >> 2);
        HEAPF32.set(floatArray, (ptr >> 2) + 1);
        setTimeout(function() { _free(ptr); }, 0);
        return ptr;
    },

    JS_FloatArrayTest2: function(callback){
        var floatArray = [0.12, 3.45, 6.789];
        var ptr = _malloc(floatArray.length * 4);  // 4 = BYTES_PER_ELEMENT
        HEAPF32.set(floatArray, ptr >> 2);
        dynCall_vii(callback, floatArray.length, ptr)
        _free(ptr);
    },

    // Fixed length Array
    JS_FloatArrayTest3: function(){
        var floatArray = [0.12, 3.45, 6.789];
        var ptr = _malloc(3 * 4);
        HEAPF32.set(floatArray, ptr >> 2);
        setTimeout(function() { _free(ptr); }, 0);
        return ptr;
    }
});

// EDIT
@jukka_j Adviced setTimeout() implemented

3 Likes

So on the js side, I got

var data = new Float64Array(3);
        data[0] = 10.5;
        data[1] = 7.3;
        data[2] = 4.9;
     
        var buf = Module._malloc(data.length * data.BYTES_PER_ELEMENT);
        Module.HEAPU8.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength), buf);

and on the C# it prints the first value(10.5), but the rest is still garbage.

EDIT
Thanks @gtk2k I will check it out. Do you know what’s wrong with my approach?

@Marks4
I think you can use HEAPF64.

var buf = _malloc(data.length * data.BYTES_PER_ELEMENT);
Module.HEAPF64.set(data), buf >> 3);

The code examples look great. If you intend for the arrays to be immediately consumed and not retained, you can defer the freeing in case 1 and 3 via

setTimeout(function() { _free(ptr); }, 0);

That way the allocated blocks will be freed up after the current rendered frame finishes. Of course this does have the issue that if you do a lot of repeated calls to a JS function within the same frame, the allocs would pile up and temporarily consume more memory than needed. In that case, it may make sense to pool the allocated ptrs for reuse within a frame.

2 Likes

@jukka_j
Thanx!

The reason why it wasn’t working is because I can’t pass the array of doubles directly, I need to use the IntPtr and Marshal like you did @gtk2k . Thanks!

@jukka_j Just curious, should I use Marshal.Copy or NativeArray.CopyTo or something else? I want the fastest way available. In other places I found that Marshal.Copy is the best way to go, but I want to confirm in the current state of Unity what is the best thing to use. Thanks!

Also I was trying to marshal to a object using

[StructLayout(LayoutKind.Sequential)]
public class cantmarshalwhy{
        public (double a, double b , double c) whycantmarshaldis;
    }

Marshal.PtrToStructure<cantmarshalwhy>(ptr, cantmarshalwhy_instance);

But I keep getting an error saying I can’t marshal the field “whycantmarshaldis”. Are tuples not supported? I searched but couldn’t find anything. Using Marshal.Copy works but would be nice if I could marshal to an instance of a class with tuples.
EDIT
Yea the problem are tuples. If I use structs instead it works.

Both Marshal.Copy and NativeArray.CopyTo should yield the same result in this case. Not sure about the rationale behind tuples - maybe C# does not define a memory layout for tuples the way it does for structs, or maybe there is something IL2CPP does not implement there. Definitely recommend using structs.

1 Like

@jukka_j @gtk2k
one last question. Why do you need to do “buf >> 3” on “Module.HEAPF64.set(data), buf >> 3)”? I know you are dividing by 8, but why do you need to do this? Why can’t the offset be 0? I thought the malloc operation already returned the offset to the initial position of the array. I tried searching this but couldn’t find anything.

JavaScript typed arrays access elements as if they are logical arrays containing aligned elements of the appropriate type, and not via absolute byte addresses. See the diagram at JavaScript typed arrays - JavaScript | MDN for an illustration.

So HEAPF64[0] accesses the first double (at byte address 0), HEAPF64[1] accesses the second double (at byte address 8) and so on.

This can cause memory access out of bounds. Don’t do it. It was hard to debug, but this is the cause. Free the memory in the C# side. For whatever reason, deferred freeing can randomly fail.

Oh wait my bad. Maybe it’s because I’m freeing a returned string. And Jukka said here

that returned strings do not need to be freed manually.