Search Unity

Question Return a float/double array from jslib to Unity

Discussion in 'Web' started by Marks4, May 26, 2021.

  1. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544
    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
    Code (CSharp):
    1. public void testDouble(double[] args) {
    2.         Debug.Log(args[0] + " " + args[1] + " " + args[2]);
    3.     }
    it logs garbage, not the values I set on the js side. Trying to use a Float64Array, if that makes any difference.
     
    Last edited: May 26, 2021
  2. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    287
    3 patterns.

    Code (CSharp):
    1. using AOT;
    2. using System;
    3. using System.Runtime.InteropServices;
    4. using UnityEngine;
    5.  
    6. public class floatArrayTest : MonoBehaviour
    7. {
    8.     [DllImport("__Internal")]
    9.     private static extern IntPtr JS_FloatArrayTest();
    10.  
    11.     [DllImport("__Internal")]
    12.     private static extern IntPtr JS_FloatArrayTest2(Action<int, IntPtr> callback);
    13.  
    14.     [DllImport("__Internal")]
    15.     private static extern IntPtr JS_FloatArrayTest3();
    16.  
    17.     [MonoPInvokeCallback(typeof(Action<int, IntPtr>))]
    18.     private static void callback(int len2, IntPtr ptr2)
    19.     {
    20.         var arr2 = new float[len2];
    21.         Marshal.Copy(ptr2, arr2, 0, len2);
    22.         Debug.Log($"JS_FloatArrayTest2 len:{len2} > {arr2[0]}, {arr2[1]}, {arr2[2]}");
    23.     }
    24.  
    25.     void Start()
    26.     {
    27.         var ptr = JS_FloatArrayTest();
    28.         var len = Marshal.ReadInt32(ptr);
    29.         var arr = new float[len];
    30.         Marshal.Copy(new IntPtr(ptr.ToInt32() + 4), arr, 0, len);
    31.         Debug.Log($"JS_FloatArrayTest len:{len} > {arr[0]}, {arr[1]}, {arr[2]}");
    32.  
    33.         JS_FloatArrayTest2(callback);
    34.  
    35.         var ptr3 = JS_FloatArrayTest3();
    36.         var arr3 = new float[3];
    37.         Marshal.Copy(ptr3, arr3, 0, 3);
    38.         Debug.Log($"JS_FloatArrayTest3 len:{3} > {arr3[0]}, {arr3[1]}, {arr3[2]}");
    39.     }
    40. }
    41.  
    Code (JavaScript):
    1. mergeInto(LibraryManager.library, {
    2.     JS_FloatArrayTest: function() {
    3.         var floatArray = [0.12, 3.45, 6.789];
    4.         var ptr = _malloc((floatArray.length + 1) * 4); // 4 = BYTES_PER_ELEMENT
    5.         HEAP32.set([floatArray.length], ptr >> 2);
    6.         HEAPF32.set(floatArray, (ptr >> 2) + 1);
    7.         setTimeout(function() { _free(ptr); }, 0);
    8.         return ptr;
    9.     },
    10.  
    11.     JS_FloatArrayTest2: function(callback){
    12.         var floatArray = [0.12, 3.45, 6.789];
    13.         var ptr = _malloc(floatArray.length * 4);  // 4 = BYTES_PER_ELEMENT
    14.         HEAPF32.set(floatArray, ptr >> 2);
    15.         dynCall_vii(callback, floatArray.length, ptr)
    16.         _free(ptr);
    17.     },
    18.  
    19.     // Fixed length Array
    20.     JS_FloatArrayTest3: function(){
    21.         var floatArray = [0.12, 3.45, 6.789];
    22.         var ptr = _malloc(3 * 4);
    23.         HEAPF32.set(floatArray, ptr >> 2);
    24.         setTimeout(function() { _free(ptr); }, 0);
    25.         return ptr;
    26.     }
    27. });

    // EDIT
    @jukka_j Adviced setTimeout() implemented
     
    Last edited: May 26, 2021
    mmdjn, adamgolden and Marks4 like this.
  3. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544
    So on the js side, I got
    Code (CSharp):
    1. var data = new Float64Array(3);
    2.         data[0] = 10.5;
    3.         data[1] = 7.3;
    4.         data[2] = 4.9;
    5.      
    6.         var buf = Module._malloc(data.length * data.BYTES_PER_ELEMENT);
    7.         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?
     
  4. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    287
    @Marks4
    I think you can use HEAPF64.
    Code (JavaScript):
    1. var buf = _malloc(data.length * data.BYTES_PER_ELEMENT);
    2. Module.HEAPF64.set(data), buf >> 3);
     
  5. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    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

    Code (JavaScript):
    1. 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.
     
    adamgolden and gtk2k like this.
  6. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    287
  7. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544
    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!
     
  8. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544
    @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

    Code (CSharp):
    1. [StructLayout(LayoutKind.Sequential)]
    2. public class cantmarshalwhy{
    3.         public (double a, double b , double c) whycantmarshaldis;
    4.     }
    5.  
    6. 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.
     
    Last edited: May 27, 2021
  9. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    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.
     
    Marks4 likes this.
  10. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544
    @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.
     
    Last edited: May 27, 2021
  11. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    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 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays 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.
     
  12. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544

    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.
     
    Last edited: Dec 30, 2021
  13. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    544