Search Unity

Trying to share float Array between c# and webgl browser javascript

Discussion in 'Web' started by dansav, Mar 30, 2016.

  1. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    I'm trying to share a float Array between c# and webgl browser javascript:

    Question 1: When I put a javascript function into jslib, does that mean it's accessible from both the webgl webpage javascript and c#?

    Question 2: How do I call a jslib function from the browser javascript? Do I use _(Underscore) or cwrap or just call it?

    Question 3: Could someone check my attempt below to create a global array of floats that would be seen by both scripts?

    jslib
    var MyPlugin = {
    var globalArray=new Array();
    CreateFloatArray: function(array, size)
    {
    for(var i=0;i<size;i++)
    globalArray=HEAPF32[(array>>2)+size]; //Confused about what this does or how it works
    }
    };
    mergeInto(LibraryManager.library, MyPlugin);​

    c#
    using UnityEngine;
    using System.Runtime.InteropServices;

    public class NewBehaviourScript : MonoBehaviour {

    [DllImport("**Internal")]
    private static extern void CreateFloatArray();

    void Start() {
    float[] globalArray = new float[10];
    CreateFloatArray(myArray, myArray.Length);
    }
    }​

    webgl browser javascript

    //here's where I get confused does globalArray already exist at this point and can I access it?
    console.log("The global Array has a length of "+globalArray.length);
    globalArray[5]=3.14; //can I change the value and have it be seen by the C# code?
    Thank you for checking out the code attempt.
     
  2. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    I'm trying the code myself based on the example on the browser to webgl communication page:

    http://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html

    I'm getting the following error when I press play.
    DllNotFoundException: **Internal
    NewBehaviourScript.Start () (at Assets/NewBehaviourScript.cs:25)

    I tried to set it up exactly as described.
    I copied the MyPlugin.jslib file into Plugins/WebGL/MyPlugin.jslib
    Made the C# script by copying what was on the page attached it to a gameObject.
     
    Last edited: Mar 30, 2016
  3. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Hello dansav.
    Yes, if you want your function to be accessible from C#, your function should be a member of the object, merged from jslib with mergeInto(LibraryManager.library, object)
    Technically, your function can be accessed from JavaScript with a preceding underscore sign, however that is not documented way of calling it, and should not be relied on (see below).

    EDIT: if you are planning to call some JavaScript functions from the page scripts or you want to define JavaScript objects, you should use files with .jspre extension, which will be appended unmodified to the WebGL JavaScript at build time.

    Well, let's better start from the very beginning.
    WebGL application runs as an asm.js module. It has its own memory space also known as module heap. Module memory heap is just a JavaScript ArrayBuffer which is accessible from JavaScript in the example above as HEAPF32. Specifically:
    Code (JavaScript):
    1. ...
    2. buffer = new ArrayBuffer(TOTAL_MEMORY);
    3. HEAP8 = new Int8Array(buffer);
    4. HEAP16 = new Int16Array(buffer);
    5. HEAP32 = new Int32Array(buffer);
    6. HEAPU8 = new Uint8Array(buffer);
    7. HEAPU16 = new Uint16Array(buffer);
    8. HEAPU32 = new Uint32Array(buffer);
    9. HEAPF32 = new Float32Array(buffer);
    10. ...
    So HEAPF32[(array>>2)+i] is just an element with index i of the 4-bytes aligned array of floats, starting at offset array (which is in fact just a number) of the Module ArrayBuffer. It is guaranteed that all the allocated elements are properly aligned, so you can just shift the offset appropriately to access them on the heap (for example HEAPF32[array>>2] or HEAPU16[array>>1] or HEAP8[array]). So basically you only need to know the type, the offset and the size of the allocated array to access it from JavaScript using HEAP variables.

    WebGL asm.js module does not have direct access to external JavaScript objects, it can only communicate with JavaScript through a set of imported functions (including functions merged from jslib plugins).

    Well, and of course, other things to fix:

    [DllImport("**Internal")] should read [DllImport("__Internal")] (a typo, thank you for notifying about it, the docs will be fixed)

    var MyPlugin = { var globalArray=new Array(); is invalid syntax because MyPlugin is an object, and even if you add globalArray member to it, it will not be merged as it is not a function. You can store variables in other namespaces though.

    CreateFloatArray(myArray, myArray.Length); myArray not defined

    float[] globalArray = new float[10]; this will not work, because this array is created locally and will be garbage collected soon, as there is no way to track dependency between a managed array and its offset on the heap, which is just a number sent to JavaScript.

    Your question however seems to be quite interesting, I think I will prepare a working example for this task and post it here.
     
    Last edited: Mar 31, 2016
  4. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Hello again dansav.

    Let's try to test the following solution.

    Create a new project, add an empty object and attach the following Assets/SharedArrayExample.cs script to it:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Runtime.InteropServices;
    4.  
    5. public class SharedArrayExample : MonoBehaviour {
    6.     [DllImport("__Internal")]
    7.     private static extern void InitJavaScriptSharedArray(float[] array, int length);
    8.     [DllImport("__Internal")]
    9.     private static extern void InitJavaScriptSharedArrayButtons();
    10.  
    11.     float[] sharedArray = {0, 0, 0};
    12.  
    13.     void Start () {
    14.         InitJavaScriptSharedArray (sharedArray, sharedArray.Length);
    15.         InitJavaScriptSharedArrayButtons ();
    16.     }
    17.  
    18.     void OnGUI() {
    19.         GUI.Label (new Rect (20, 20, 500, 100), sharedArray[0].ToString() + " " + sharedArray[1].ToString() + " " + sharedArray[2].ToString());
    20.     }
    21. }
    Now add the following Assets/Plugins/plugin.jslib file:
    Code (JavaScript):
    1. mergeInto(LibraryManager.library, {
    2.   InitJavaScriptSharedArray: function(byteOffset, length) {
    3.     JavaScriptSharedArray = new Float32Array(buffer, byteOffset, length);
    4.   },
    5.   InitJavaScriptSharedArrayButtons: function() {
    6.     for(var i = 0; i < JavaScriptSharedArray.length; i++) {
    7.       var button = document.createElement('button');
    8.       button.index = i;
    9.       button.innerHTML = i;
    10.       button.onclick = function() { JavaScriptSharedArrayIncrement(this.index); }
    11.       document.body.appendChild(button);
    12.     }
    13.   }
    14. });
    And also the following Assets/Plugins/jsFunctions.jspre file (just to demonstrate how to interact with external JavaScript):
    Code (JavaScript):
    1. function JavaScriptSharedArrayIncrement(index) {
    2.   JavaScriptSharedArray[index]++;
    3. }
    After you build it you should be able see the following 0, 1, 2 html buttons in the upper-left corner, which increment the corresponding elements of the JavaScriptSharedArray when you click them, while the changes are immediately reflected in the C# sharedArray:

    sharedArray.png

    So the trick is that it is in fact the very same memory: JavaScriptSharedArray is just a mapped view of the Module heap ArrayBuffer block where the real sharedArray resides. This should work just fine under assumption that C# array does not get reallocated or garbage collected.
     
    Last edited: Apr 11, 2016
  5. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    Thank you for creating an easy to understand demonstration of sharing an array between webgl and the browser javascript. This is extremely cool.

    Also thanks for explaining the theory behind how it works. You are an excellent teacher.
     
  6. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    Is it possible to use the same tricks to share other things like:
    A string?
    An Array of Strings?
    I tried investigating but it seems like strings are fundamentally different.
     
  7. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Thank you for the kind words. I hope this will help you create some good stuff in WebGL.

    In C# strings are immutable, which means the content of the actual string object cannot be changed from C# after string is created. Each time you modify your string with = or += operators for example, a new string object is created and assigned to your variable, while the old one is garbage collected. So technically you can probably share the string if you limit access from C# only to read operations.

    Note that while memory sharing between C# and JavaScript might seem to be a very attractive idea, you should use it wisely. In most cases you are able to just transfer the address of the C# object to JavaScript explicitly as an additional parameter in each function call, this way you wont have to worry about the C# object reallocation. Small objects like strings can be efficiently allocated on the module stack from JavaScript and transferred to C# using SendMessage. Memory sharing however can still be considered in performance critical situations, for example, when you have tens of thousands C#/JavaScript interactions per second, which would imply noticeable overhead if performed through function calls.
     
  8. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    Thanks for explaining the theory. It sounds like I could share an array of string pointers using the same method. If I could get the pointers to all the strings? Or could I still not change them from both sides?

    What are the limits of what we can share? Can we share the address of an Object in C# ?
    Let's say I create a class called Car. Could I share a pointer to mycar and access all of it's properties somehow?

    public class Car{
    private string make;
    private float cost;
    private int miles;
    }
    mycar =new Car();
    mycar.make="Honda";
    mycar.cost="10000.00;
    mycar.miles="100000";
     
  9. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Answering the first part of your question.
    You can not change the contents of a C# string object from C# as it is immutable, means that after performing any operation with a string in C# you should assume that the string has been reallocated and the address of the actual string characters has changed. There is a bit more freedom on the JavaScript side, as you can change the characters (assuming that you got the address) without breaking anything, but still, it would be not possible to change the length of the string. Note that in C# strings are not null-terminated so they also have length. So how are you going to deal with the situation when the length of the string changes after modification? While string size reduction can be handled in some way, there is not much you can do in case of increased string size. Unless you allocate large enough buffer that can handle any string length possible in your use case. Then you will end up with something like a shared byte buffer for a string and BinaryReader/BinaryWriter interfaces for shared strings in C#. Well, in some situations it might indeed give you somewhat higher performance, as string C# interface will then be internal to asm.js, but it would be much easier to just store the strings only on one side (either in C# or JavaScript) and add the accessor interface for another.
     
  10. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    Thanks for the additional information and theory. I can use the mirrored string idea. That solution along with the float and integer arrays should be enough to solve the fundamental problems for my project. I can break the object information into arrays of shared floats and integers, and then put it back together each frame. I'm hoping that process will not take too long for large numbers of objects. Some of what I would like to do would be some scientific simulations which might have large numbers of objects.
     
  11. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
  12. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    No, you can not use this mechanism directly for C#. As has been noted before, C# scripts are first transformed into C++ code using il2cpp. There is no direct correspondence between C# classes and their generated C++ representations, and even if there was, it would be Unity version specific, and therefore you would not be able to rely on it. It should be however possible to achieve something like this using C plugin.

    Note that mentioned C++ binding does not involve sharing of the actual class objects or memory. When you use instance.x=20 in JavaScript from the above example, you do not modify the x value on the heap directly, but instead implicitly execute the JavaScript setter function of the instance, which then transfers your value 20 to the binded &MyClass::setX C++ function. So this only looks like sharing while in fact this is copying. This mechanism has been implemented for programming convenience and you should not expect any performance boost when using it. Moreover, you can implement the same accessor functions for your C# classes yourself (in which case you will also have to add the binding code to the JavaScript manually).
     
  13. dlchang

    dlchang

    Joined:
    Aug 25, 2016
    Posts:
    1
    Hello, I was wondering if this works for Uint8 arrays (on the javascript end) and byte[ ] (on the C# end)?
    For some reason, I can't get the C#'s byte [ ] (the Unity application in the browser) to reflect the same data as the javascript's uint8 array, even though they should technically be in the same location in memory.

    My scene only has a text object and main camera. This is the script, which is attached to the main camera.

    Code (csharp):
    1.   public class TextChanger : MonoBehaviour {
    2.  
    3.  public Text banner;
    4.  private int display;
    5.  private byte[] freqs;
    6.  //Usethisfor initialization
    7.  void Start () {
    8.  display = 0;
    9.  freqs = new byte[1024];
    10.  initSharedArray (freqs);
    11.  }
    12.  
    13.  //Updateiscalledonceper frame
    14.  void FixedUpdate () {
    15.  banner.text = display.ToString();
    16.  getFreqs (freqs);
    17.  display = freqs[0];
    18.  }
    19.  
    20.  [DllImport("__Internal")]
    21.  private static extern void initSharedArray(byte[] array);
    22.  
    23.  [DllImport("__Internal")]
    24.  private static extern void getFreqs(byte[] array);
    25. }
    This is the jslib plugin, named MyPlugin.jslib and stored in Assets/Plugins/WebGL:

    Code (csharp):
    1.  
    2. var MyPlugin = {
    3.     initSharedArray: function(byteOffset){
    4.         buffer = new ArrayBuffer(TOTAL_MEMORY);
    5.         sharedArray = new Uint8Array(buffer,byteOffset,1024);
    6.     },
    7.     getFreqs: function(byteOffset)
    8.     {
    9.     if (window.freqByteData){
    10.         for(var i = 0; i < 1024; i++)
    11.             sharedArray[i] = this.freqByteData[i];
    12.         sharedArray[0] = 5;
    13.      }else{
    14.      console.log("has not started yet!");
    15.      }
    16.     }
    17. };
    18.  
    19. mergeInto(LibraryManager.library, MyPlugin);
    20.  
    When I check in the console, I see this:

    Code (csharp):
    1.  
    2. buffer =  new ArrayBuffer(TOTAL_MEMORY);
    3. HEAPU8 = new Uint8Array(buffer);
    4. >> Uint8Array[268435456]
    5. HEAPU8[sharedArray.byteOffset]
    6. >> 5
    7. total = 0;for (i=0; i< 1024; i++){total+=HEAPU8[sharedArray.byteOffset+i];}
    8. >> 374
    9.  
    I'm pretty confused as to why freqs seems to be staying the same and not reflecting the changed values?
     
  14. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Hello dlchang.

    Without getting into other details, pay attention to the following:
    Code (JavaScript):
    1. buffer = new ArrayBuffer(TOTAL_MEMORY);
    Creates a new ArrayBuffer of size TOTAL_MEMORY and assigns it to the buffer variable. This new buffer is not related to the original module heap. Note that the code might still work well even though you have changed the value of the buffer variable, because if most places the code uses views and references to the original buffer (and not the buffer variable directly).

    On the other hand:
    Code (JavaScript):
    1. sharedArray = new Uint8Array(buffer,byteOffset,1024);
    Creates a view of the ArrayBuffer specified by the buffer (which represents the heap). The buffer variable is global, if you want to access the heap memory you just need to create a view of the buffer (or use an existing global view like HEAP8, HEAP32 etc., which start from offset 0 in the heap). You are not supposed to change the buffer variable value.

    You may also check the https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray for the details.
     
    Last edited: Sep 1, 2016
  15. theylovegames

    theylovegames

    Joined:
    Aug 18, 2012
    Posts:
    176
    Thanks for all the details in this post. Shared memory between the jslib and Unity C# is working nicely!
     
  16. specweapons

    specweapons

    Joined:
    Dec 23, 2016
    Posts:
    7
    And also the following Assets/Plugins/jsFunctions.jspre file (just to demonstrate how to interact with external JavaScript):
    Code (JavaScript):
    1. function JavaScriptSharedArrayIncrement(index) {
    2.   JavaScriptSharedArray[index]++;
    3. }
    Very, Very , Very important info why isn't it in the Documentation say like right about here somewhere?
    https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html

    Uggg, thanks, I remember reading this, but then when I needed it took me like hours to find the .jspre reference again.
    If however it had been documented under the webgl section close to browser scripting I would have saved some time.

    Thanks
     
  17. nsmith1024

    nsmith1024

    Joined:
    Mar 18, 2014
    Posts:
    870
    wrap it all up in a json string
     
  18. KalyaniKolape

    KalyaniKolape

    Joined:
    May 21, 2019
    Posts:
    3

    I need to pass array from C#. Write data to that array in jslib. And access those values in C# again Is it possible?
     
  19. booferei

    booferei

    Joined:
    Sep 29, 2016
    Posts:
    21
    Yes.
     
  20. booferei

    booferei

    Joined:
    Sep 29, 2016
    Posts:
    21
  21. dansav

    dansav

    Joined:
    Sep 22, 2005
    Posts:
    510
    I'm sharing 100 floats between unity webgl and javascript using the methods described above. In javascript I can see that the shared memory is filled with zeros. In unity at some point I generate a large 100Kb string which is initially set to be empty. When I do that the shared memory is emptied. I also get a javascript error about the array being detached. Is this because the string is changing the size of the total buffer?

    Here's my setup

    Unity
    Code (CSharp):
    1.     public float[] SharedSound;
    2.         SharedSound = new float[100];        
    3. CreateSharedSound (SharedSound, SharedSound.Length);
    4.  
    5. public string s;
    6. s="";
    7.  
    8. void createLargeString(){
    9. //creates a large string that seems to destroy the SharedSound Array
    10. s=largestring;
    11. }
    MyPlugin.jslib
    Code (JavaScript):
    1. CreateSharedSound: function(byteOffset, length) {
    2.         SharedSound = new Float32Array(buffer, byteOffset, length);
    3.       },    
    Javascript:
    Code (JavaScript):
    1. console.log(SharedSound);
    2. //gives back a float array filled with 0's until the createLargeString is run then it gives back an empty array.
    I've traced the problem down to this one function, all it does it make a large string from a bunch of data. Is the resizing of the string causing the buffer to change and disconnecting my SharedSound memory? How do I prevent this?
     
    unnanego likes this.
  22. Marks4

    Marks4

    Joined:
    Feb 25, 2018
    Posts:
    547
    So it's impossible to send an array to javascript without it being global? What about a struct? Is there a way to send a local struct to javascript and have it read there? Or it must be a class field as well?
     
  23. adamgolden

    adamgolden

    Joined:
    Jun 17, 2019
    Posts:
    1,555