Search Unity

C# - .jslib 2-way communication

Discussion in 'Web' started by DudeGuy, May 4, 2015.

  1. DudeGuy

    DudeGuy

    Joined:
    Jul 17, 2014
    Posts:
    19
    Hello,

    In my WebGL targeted project, I currently have a .jslib file I'm using so that I can access javascript methods from my C# code. This all works great.

    What I'm hoping to be able to do is to call back from the .jslib file into my C# code. I'm ok with either having a direct method to call from javascript or having a delegate/System.Action passed in from the C# side of things. I don't know how to handle either situation.

    Here is a simple example of what I'd like to do from the C# side:

    private System.Action action = null;

    [DllImport("__Internal")]
    private static extern void DoSomething(System.Action callback); // Or delegate if necessary.

    Is this something that is possible and how would I manage it on the javascript side if so?

    Thanks!
     
  2. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    liortal likes this.
  3. DudeGuy

    DudeGuy

    Joined:
    Jul 17, 2014
    Posts:
    19
    Doh, for some reason I got myself under the impression that SendMessage wasn't possible in WebGL. I see, the limitation is that it has to be a MonoBehaviour. I can make it work! I appreciate the reply and sorry for the inconvenience!
     
    Last edited: May 5, 2015
  4. gfoot

    gfoot

    Joined:
    Jan 5, 2011
    Posts:
    550
    SendMessage sucks a bit. If you want your Javascript to call the C# directly without just queuing a message, e.g. if you want your C# to return something, then you can pass callbacks like in your original post, so long as you jump through a few hoops in both the C# and the jslib.

    Here's one I prepared earlier:

    Code (CSharp):
    1. using System;
    2. using System.Runtime.InteropServices;
    3. using AOT;
    4. using UnityEngine;
    5.  
    6. public class JsCallCsTest : MonoBehaviour
    7. {
    8.     [DllImport("__Internal")]
    9.     public static extern void ProvideCallback(Action action);
    10.  
    11.     public void Start()
    12.     {
    13.         ProvideCallback(Callback);
    14.     }
    15.  
    16.     [MonoPInvokeCallback(typeof(Action))]
    17.     public static void Callback()
    18.     {
    19.         Debug.Log("Callback called");
    20.     }
    21. }
    And the jslib:

    Code (JavaScript):
    1. var LibraryJsCallCsTest = {
    2.     $JsCallCsTest: {},
    3.  
    4.     ProvideCallback: function(obj)
    5.     {
    6.         console.log("ProvideCallback");
    7.         console.log(obj);
    8.         JsCallCsTest.callback = obj;
    9.         Runtime.dynCall('v', obj, 0);
    10.     },
    11. };
    12.  
    13. autoAddDeps(LibraryJsCallCsTest, '$JsCallCsTest');
    14. mergeInto(LibraryManager.library, LibraryJsCallCsTest);
    That Runtime.dynCall is the delicate bit. obj is an integer index into a lookup table containing function pointers passed from C++ to Javascript. There's a separate table for each function signature, so in order for it to look in the right table, you need to specify the function signature - that's what the 'v' bit is for, it's one letter for the return type, then one more letter per parameter. 'v' means void (only for the return value). The other commonly-used letters seem to be i (integer), f (float), and d (double).

    You can search your generated code for FUNCTION_TABLE_ to see the various function tables that exist, and you might be able to find your callback adaptor function in the table if you can figure out what to look for, in case you're not sure what to specify for the signature.
     
    Last edited: May 6, 2015
    DMorock, xucian, eheimburg and 12 others like this.
  5. jonas-echterhoff

    jonas-echterhoff

    Unity Technologies

    Joined:
    Aug 18, 2005
    Posts:
    1,666
    Yes, what gfoot wrote will also work - it requires more boilerplate, but may be the more elegant solution, depending on what you need.
     
  6. DudeGuy

    DudeGuy

    Joined:
    Jul 17, 2014
    Posts:
    19
    This is fantastic! Thanks much! I haven't had the time to mess around with this yet today, but I plan to do so very soon! Is it possible to do anything like this on a class that is not a MonoBehaviour?

    I'm pretty new to javascript in general and so I'm curious about the javascript portion you have there which is:

    $JsCallCsTest: {},

    Does this somehow link itself to the class? And should it be JSCallCsTest.Callback instead of .callback? Either way, I plan to mess around with this very soon.

    Thanks again all!
     
  7. gfoot

    gfoot

    Joined:
    Jan 5, 2011
    Posts:
    550
    Yes it is nothing to do with MonoBehaviour. It only works for static methods, at least according to the docs for MonoPInvokeCallback. I don't know whether that restriction still applies with IL2CPP.

    The $JsCallCsTest variable is a matter of habit, it is not really used there. It is an empty object that can contain data shared between the various functions exported by the jslib.

    The idea was to store the callback for future reference and actually call it from somewhere else, but I guess I just made it call it straight away in the end.
     
  8. DudeGuy

    DudeGuy

    Joined:
    Jul 17, 2014
    Posts:
    19
    Oh ok, very nice, I see what you're saying. You pretty much create an empty object and dynamically add a "callback" field to it. So it works similar to python in that regard. (Sorry, still new to javascript).

    That is great that it doesn't have to work on a MonoBehaviour and I do remember seeing all of the 'v', 'vi', 'iii', etc in code. Wasn't aware that's what it stood for until this thread. This seems doable!

    So thanks again for the help!
     
  9. RostislavS

    RostislavS

    Joined:
    Jun 17, 2015
    Posts:
    4
    Big Thanks for that !

    But how can I pass 'string' to my Callback as a parameter?
     
  10. crushforth

    crushforth

    Joined:
    Jul 22, 2010
    Posts:
    113
    @jonas echterhoff & @gfoot I think I need to use this method of communicating back to the C# from javascript because SendMessage (that I'm currently using) fails when the WebGL is packaged into a chrome web app because of how the security is different (can't use eval or any string to method functionality)

    I'm not fully understanding the above example (and it seems this isn't widely used). How do I actually trigger the callback from normal webpage javascript.

    Code (csharp):
    1.  
    2. var LibraryJsCallCsTest = {
    3.     $JsCallCsTest: {},
    4.    
    5.     ProvideCallback: function(obj) {
    6.         console.log("ProvideCallback");
    7.         console.log(obj);
    8.  
    9.         JsCallCsTest.callback = obj;
    10.     },
    11.  
    12.     TriggerCallback: function() {
    13.         Runtime.dynCall('v', JsCallCsTest.callback, 0);
    14.     },
    15. };
    16. autoAddDeps(LibraryJsCallCsTest, '$JsCallCsTest');
    17. mergeInto(LibraryManager.library, LibraryJsCallCsTest);
    18.  
    To trigger the function would you have to use the library name like LibraryJsCallCsTest.TriggerCallback(); I'll do some experiments (I'm mostly asking as the iteration time on WebGL can be quite lengthy).
     
  11. alexsuvorov

    alexsuvorov

    Unity Technologies

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

    You can face a security restriction when calling JavaScript from C# using Application.ExternalCall() or Application.ExternalEval().

    EDIT: it appears that SendMessage also involves eval and therefore will also cause security exception

    Of course, there are some situations when you need a special way of interaction, i.e. for performance reasons. Please attach your original interaction code (both C# without Application.ExternalCall() or Application.ExternalEval(), and JavaScript with SendMessage) to make sure that it really is the case for you.

    The solution described above involves direct access to the function tables, which is implementation-specific and therefore may require some support if Emscripten code changes. There is also another interesting solution provided by atti which seems to be more implementation-independent to me: http://forum.unity3d.com/threads/super-fast-javascript-interaction-on-webgl.382734/
     
    Last edited: Feb 2, 2016
  12. crushforth

    crushforth

    Joined:
    Jul 22, 2010
    Posts:
    113
    Hi Alex, I implemented your suggestion yesterday about how to get around using eval using a jslib and not using Application.ExternalCall. This all worked perfectly going in that direction now my problem is once the youtube video finishes playing I need to signal back to the unity code that the iframe/webview has been closed and the app need to continue.

    I just need any method that wont fail in a packaged chrome app of signalling back to unity. Was thinking of maybe using player prefs or something and monitoring a flag.
     
  13. crushforth

    crushforth

    Joined:
    Jul 22, 2010
    Posts:
    113
    I'll re-implement the SendMessage version and show you the error. Maybe I interpreted it incorrectly.
     
  14. crushforth

    crushforth

    Joined:
    Jul 22, 2010
    Posts:
    113
    It does seem to be an issue with either SendMessage or cwrap doing an unsafe-eval.

    Screen Shot 2016-02-02 at 2.23.15 PM.png

    The window.alert is I believe unity trying to she the exception and it being blocked in a chrome app.
     
  15. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Yes, you are absolutely right. It appears that cwrap involves eval.
    Nevertheless, I believe this can be easily resolved. I can not test on chrome app right now, but here is the idea how to eliminate eval from the SendMessage. Add the following script into your index.html:
    Code (CSharp):
    1. ...
    2. <script src="Release/UnityLoader.js"></script>
    3. <script>
    4.   Module.onRuntimeInitialized = function() {
    5.     SendMessage = function(gameObject, func, param) {
    6.       if (param === undefined)
    7.         Module.ccall("SendMessage", null, ["string", "string"], [gameObject, func]);
    8.       else if (typeof param === "string")
    9.         Module.ccall("SendMessageString", null, ["string", "string", "string"], [gameObject, func, param]);
    10.       else if (typeof param === "number")
    11.         Module.ccall("SendMessageFloat", null, ["string", "string", "number"], [gameObject, func, param]);
    12.       else
    13.         throw "" + param + " is does not have a type which is supported by SendMessage.";
    14.     }
    15.   }
    16. </script>
    17. ...
    This should override the default SendMessage and eliminate the cwrap call. Let me know if this resolved the situation or if some additional steps are required.
     
    crushforth likes this.
  16. crushforth

    crushforth

    Joined:
    Jul 22, 2010
    Posts:
    113
    Thanks Alex, You're quickly becoming one of my favourite Unity employees. Your help and suggestions over the last few days have got me out of some very sticky situations. Very much appreciated.

    That did fix my issue. Would be great to get this implemented in future Unity versions. If you need me to create an issue/bug I'd be glad to.
     
  17. alexsuvorov

    alexsuvorov

    Unity Technologies

    Joined:
    Nov 15, 2015
    Posts:
    327
    Thank you very much for the kind words.
    This will surely be implemented in the future versions of Unity. Feel free to create a bug report (also please post the case number here).

    By the way, if you don't want to deal with index.html or WebGL templates, you can also just call
    Module.ccall("SendMessage", null, ["string", "string"], [gameObject, func]),
    Module.ccall("SendMessageString", null, ["string", "string", "string"], [gameObject, func, param]) or
    Module.ccall("SendMessageFloat", null, ["string", "string", "number"], [gameObject, func, param])

    instead of SendMessage(gameObject, func, param) directly from your jslib plugin. I believe the result will be pretty much the same.
     
    Last edited: Feb 3, 2016
  18. Schubkraft

    Schubkraft

    Unity Technologies

    Joined:
    Dec 3, 2012
    Posts:
    1,073
    We've got the bug and I've passed it on. Thanks for submitting it.
     
  19. Thaina

    Thaina

    Joined:
    Jul 13, 2012
    Posts:
    1,163
    Can we use Module.ccall to call static function in C# directly?

    Are there anyway to just call C# function directly without SendMessage?
     
  20. alexsuvorov

    alexsuvorov

    Unity Technologies

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

    Please check out the following topic http://forum.unity3d.com/threads/super-fast-javascript-interaction-on-webgl.382734/
    There you can find an explanation why C# function can not be called directly, and a working solution proposed by atti, demonstrating how to call a static C# function from JavaScript using C plugin.
     
    Thaina likes this.
  21. Thaina

    Thaina

    Joined:
    Jul 13, 2012
    Posts:
    1,163
    Now I was trying to play with this feature. It somehow success but I'm so curious why we can only use static method with AOT.MonoPInvokeCallBack. I wish we could just send function pointer from delegate here

    More and more I try to use webgl on unity. More and more I felt that unity never want us to use webgl
     
  22. Nickname219

    Nickname219

    Joined:
    Mar 16, 2014
    Posts:
    2
    Try this:

    Code (CSharp):
    1. using AOT;
    2. using System.Runtime.InteropServices;
    3. using UnityEngine;
    4.  
    5. public class SimpleScript : MonoBehaviour
    6. {
    7.     public delegate void SimpleCallback(System.IntPtr ptr);
    8.  
    9.     [DllImport("__Internal")]
    10.     private static extern void SimpleMethod(SimpleCallback callback);
    11.  
    12.     private void Start()
    13.     {
    14.         SimpleMethod(Callback);
    15.     }
    16.  
    17.     [MonoPInvokeCallback(typeof(SimpleCallback))]
    18.     public static void Callback(System.IntPtr ptr)
    19.     {
    20.         string value = Marshal.PtrToStringAuto(ptr);
    21.         Debug.Log("Managed string: " + value);
    22.     }
    23. }
    and jslib:

    Code (JavaScript):
    1. var SimplePlugin = {
    2.  
    3.     SimpleMethod: function(obj) {
    4.    
    5.         var buffer = getPtrFromString('Hello, world!');
    6.         Runtime.dynCall('vi', obj, [buffer]);
    7.        
    8.         function getPtrFromString(str) {
    9.             var buffer = _malloc(lengthBytesUTF8(str) + 1);
    10.             writeStringToMemory(str, buffer);
    11.             return buffer;
    12.         }
    13.     }
    14. };
    15.  
    16. mergeInto(LibraryManager.library, SimplePlugin);
     
    nlprog, Thaina, GilCat and 3 others like this.
  23. OrderOfOut

    OrderOfOut

    Joined:
    Sep 21, 2018
    Posts:
    37
    So here's an issue that I don't think .jslib has solved. I have two independent apps that need to talk to each other -- one javascript app that uses jquery, pixi, and a number of other libraries, and one Unity WebGL app. They do separate functions but occasionally need to pass data back and forth in the same browser window. How am I supposed to call a javascript function from unity that, say, has jquery lines in it, using this method? It seems to me that I would have to stuff jquery, pixi, and my entire javascript app into .jslib files, which is absurd. ExternalCall did this perfectly well, and since this is not going to be a public project (it's for a scientific study with maybe 50-100 users), I see no reason not to stick with that. Unless I'm misunderstanding something, which is totally possible!
     
  24. AlexHell

    AlexHell

    Joined:
    Oct 2, 2014
    Posts:
    167
    If they stay in the same html page, then you can use JS<->JS interaction. You can place all of your public classes / functions in window scope (pixi, jquery, controllers for change html etc) and move this js-files to Template folder (for stay in your directory of game when you rebuild, otherwise you can make this scripts outside, but make sure to include this js files to html page and your redist of game)
    Then you can call JS-native (from Template, or not.. simple included in html page) from JS-plugin (in jspre and jslib extensions) and vise-versa
    Also you can call JS-plugin<->C# etc
     
  25. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    676
    What a about passing back 2 strings? Is this possible with passing 2 buffers or should it be concat in one big string?

    Trying:
    Code (CSharp):
    1. Runtime.dynCall('vii', obj, [buffer1, buffer2])
    Fails miserably.

    Thanks
     
  26. De-Panther

    De-Panther

    Joined:
    Dec 27, 2009
    Posts:
    589
    Try
    Runtime.dynCall('vii', obj, buffer1, buffer2);

    But it seems that there were some changes in emscripten, and you might want to try
    Module['dynCall_vii'](obj, buffer1, buffer2);
     
    Flavelius and GilCat like this.
  27. karmatha

    karmatha

    Joined:
    Aug 25, 2012
    Posts:
    50
    Both of these didn't seem to work for me to be honest. I resorted to concatining the two strings and splitting them again in c#..
     
  28. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
    I was able to pass two strings with this code.
    (Unity 2019.4.1f1)

    Code (CSharp):
    1. [DllImport("__Internal")]
    2. private static extern void cbTest(Action<string, string> cb);
    3.  
    4. private void Start() {
    5.     cbTest(cbStrStr);
    6. }
    7.  
    8. [MonoPInvokeCallback(typeof(Action<string, string>))]
    9. private static void cbStrStr(string str1, string str2)
    10. {
    11.     Debug.Log($"str1:{str1}, str2:{str2}");
    12. }
    Code (JavaScript):
    1. mergeInto(LibraryManager.library, {
    2.     cbTest: function(cb) {
    3.         var str1 = 'foo';
    4.         var len1 = lengthBytesUTF8(str1) + 1;
    5.         var strPtr1 = _malloc(len1);
    6.         stringToUTF8(str1, strPtr1, len1);
    7.         var str2 = 'bar';
    8.         var len2 = lengthBytesUTF8(str2) + 1;
    9.         var strPtr2 = _malloc(len2);
    10.         stringToUTF8(str2, strPtr2, len2);
    11.         Module.dynCall_vii(cb, strPtr1, strPtr2);
    12.     }
    13. });
    I have a question.
    In the documentation
    ʻIf the string is a return value, then the il2cpp runtime will take care of freeing the memory for you`
    Is it necessary to do _free() in this case?
    How do I run _free() if I need to?
     
    Last edited: Oct 2, 2020
    mdrunk, mmntlh, DMorock and 3 others like this.
  29. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    Thanks for posting a great example!

    No need to call _free() here. When a return value is a string, it *must* be allocated via _malloc(), and IL2CPP will take responsibility of calling _free() on the string memory.
     
    gtk2k likes this.
  30. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
    Thanx! @jukka_j
    If it's not a string, do I need to run _free()?
     
  31. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    No, integers, floats and doubles do not need to be freed. Only pointers returned by a call to _malloc() need to be freed, with that only exception of returning a string from a function to C#. The exception is there because otherwise there would be no way to free up the memory afterwards - passing of ownership must occur in that scenario.
     
    Marks4 likes this.
  32. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
    @jukka_j
    Thax!

    There is another question I had after posting the question.
    What if I return an Array (Typed Array) type?
     
    mdrunk likes this.
  33. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    Because WebAssembly operates on a single typed array heap only and cannot address other typed arrays, it is not possible to return a typed array, but one will need to first _malloc() an array, then write the elements out to that array. Check out Emscripten documentation site on how to access HEAP8/HEAP16/HEAP32/HEAPF32/HEAPF64 to write to arrays from Javascript.
     
    mdrunk and De-Panther like this.
  34. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
  35. bruceb85

    bruceb85

    Joined:
    Sep 26, 2020
    Posts:
    45
    how can one call a function in another .js file from the .jslib?

    I can call functions in .jslib from c# but can't get .jslib to call my .js
     
  36. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    You should be able to. From a JS function in a .jslib file, the whole global JS context is there, so whatever scripts one defines in the main .html file are visible to the .jslib content. If that is not working, then something is wrong with either the JS code on the .jslib side, or on the main .html side.

    Perhaps you can make a small example that is not working for you and ask in the forums, maybe someone will catch what is going wrong.
     
  37. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
    I want to write jslib with es6+ syntax:)
     
  38. jukka_j

    jukka_j

    Unity Technologies

    Joined:
    May 4, 2018
    Posts:
    953
    That is a good suggestion. Currently the compiler version we are using runs some JS optimizer passes that only manage ES5 code. Added a note to test out how well ES6 will work once we finish updating our compiler version.
     
    karmatha, De-Panther and gtk2k like this.
  39. travlake

    travlake

    Joined:
    Oct 4, 2019
    Posts:
    50
    Quick question this raises: I know you need to do the _malloc thing when returning strings, but do you also need to do it when calling Module.SendMessage or Module.dynCall_vii ?

    I'm confused because @gtk2k does below but this guy wrote a whole library where he uses SendMessage callbacks passing strings and never uses _malloc at all

    https://github.com/rotolonico/FirebaseWebGL/tree/master/Assets/FirebaseWebGL/Plugins

     
  40. gtk2k

    gtk2k

    Joined:
    Aug 13, 2014
    Posts:
    288
    @travlake
    If you want to pass a string with dynCall_xxxxx(), you need to do _malloc(), but when use SendMessage() does not need.
     
    travlake and De-Panther like this.
  41. grizzlycorv

    grizzlycorv

    Joined:
    Nov 9, 2018
    Posts:
    19
    What is the difference between the two and when do I use what?
     
  42. Thaina

    Thaina

    Joined:
    Jul 13, 2012
    Posts:
    1,163
    It seem `Runtime.dynCall` is the old version of emscripten. `Module['dynCall_vii']` is just newer version move the api around. You should try both and log the result to see which one is exist in your build
     
    grizzlycorv likes this.
  43. grizzlycorv

    grizzlycorv

    Joined:
    Nov 9, 2018
    Posts:
    19
    I'm using 2020.3.0f1 and Runtime.dynCall seems to work although I get weird errors like this: https://forum.unity.com/threads/typeerror-npa-is-not-a-function.1078505/

    I'll try Module['dynCall_vii'] and see if it changes something.