0

Good afternoon, dear colleagues. I'm trying to adapt a Unity WEBGL program for a some social network on the WEB. The social network has an API and integrated simply with Unity via JavaScript. It is possible to store user data to variables that will be stored on social network. I have written an interface on Unity that allows to interact with a social network, but there is a problem that I cannot solve. The Unity program uses the following code to get stored data:


    public class PlayerPrefsMapProgressManager : IMapProgressManager
    {
    
       // some code
    
        public int LoadLevelStarsCount(int level)
        {
            return PlayerPrefs.GetInt(GetLevelKey(level), 0); 
        }
    }

I want to replace PlayerPrefsfunction on my own. In my code, it consists of two parts, the first function sends a get request, then the social network API calls the call back function with the returned value.


bridge.JSStorageGet("scope", ResultStorageGet);

    public void ResultStorageGet(string value)
    {
        Debug.Log("Got value: " + value);
    
    }

When I try to replace in the source code, the callback function does not have time to return the value:

    public class PlayerPrefsMapProgressManager : IMapProgressManager
    {
    
    // some code
      string Data;
    public JSBridgeController bridge;
     public void ResultStorageGet(string value)
    {
        Data =  value;
    
    }
    
        public int LoadLevelStarsCount(int level)
        {
        bridge.JSStorageSet("scope", "100");
        // I thing should be delay here,  ResultStorageGet finished later
            return Data; // return empty value 
        }
    }

How can I make a delay to wait for the call back function to be called, or how can I do it differently? Ideas, examples, links. I 've tried everything I can .

Full text of the interaction with the API of the site:


// Test.cs file ****************************************


public class Test : MonoBehaviour
{
    public JSBridgeController bridge;
 
    public void ClickStoradgeGet() // click some button
    {
        bridge.JSStorageGet("scope", ResultStorageGet);
    }
        public void ResultStorageGet(string value)
        {
            Debug.Log("Got value: " + value);

        }
    public void ClickStoradgeSet() // click some button
    {
        bridge.JSStorageSet("scope", "100");
    }

}

//JSBridgeController.cs file ***************************************************

public class JSBridgeController : MonoBehaviour
{
    [DllImport("__Internal")]
    public static extern void _JSStorageSet(string key, string value);
    [DllImport("__Internal")]
    public static extern string _JSStorageGet(string key);
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    public void JSStorageSet(string key, string value)
    {
#if !UNITY_EDITOR
        _JSStorageSet(key, value);
#endif
    }

    public void JSStorageGet(string key, UnityAction<string> action)
    {
#if !UNITY_EDITOR
        // TODO : Вместо стринга сделать отдельную структуру, в которой будет отслеживаться ошибка
        _actionStorageGet = action;
        _JSStorageGet(key);

#endif

    }
    public UnityAction<string> _actionStorageGet;

}

// JSBridgeHandler.cs file ********************************************
public class JSBridgeHandler : MonoBehaviour
{
    private JSBridgeController controller;
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        controller = this.GetComponent<JSBridgeController>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void ResultStorageGet(string result)
    {
        controller._actionStorageGet.Invoke(result);
    }
}

// jsbridge.jslib file ******************************************************

mergeInto(LibraryManager.library, {
  _JSStorageSet: function (str, value) {
    // Deal with api
    console.log(UTF8ToString(value));
  },
  _JSStorageGet: function (str) {
    // Deal with api and callback 
        ss.SendMessage('JSBridge', 'ResultStorageGet', "5");
  }

});

Thanks you very much for the reply. The int option looks better and simpler. Faced with the following problem. The API code inside the _JS StorageGet function is executed asynchronously and fineshed after calling return value, i.e. _JS Storage Get returns an empty value. How can I wait for the value to be received in the example, or any other solution?

_JSStorageGet: function (keyPtr, fallbackValue) {

    // you first need to actually read them out!
    var keyString = UTF8ToString(keyPtr);

    // Deal with api  

       myBridge.send("StorageGet", {"keys": [keyString]}).then(data => {
       if (data.keys[0].value) {
             console.log(JSON.parse(data.keys[0].value));
            var value = JSON.parse(data.keys[0].value);
            return value ; // return too early, when value is not ready yet
    } else 
    return fallbackValue;
  }

Additional Information.

I think i don't explain my question correctly. Try again. I put console alert in code:

  _JSStorageGet: function (keyPtr, fallbackValue) {

    // you first need to actually read them out!
    var keyString = UTF8ToString(keyPtr);

    // Deal with api and callback 

    //return fallbackValue;
   console.log('_JSStorageGet complete');
    return 42;
  }
    public static int GetInt(string key, int fallback = 0)
    {
    int retval; 
        retval = _JSStorageGet(key, fallbackValue);
        Debug.Log(" GetInt: " + retval );
    return retval;
    }

if run GetInt I got:

GetInt: 0
_JSStorageGet complete

I'm expecting

_JSStorageGet complete
GetInt: 42 
0

1 Answer 1

0

It is significantly easier to pass int forth and back between c# and JS than a string!

Mainly because int has a fixed byte length so you can plain share the memory, string however is an array with dynamic length!

=> If you want an int anyway, then stick to int!

public static class APIController
{
    // personal preference: Keep these private ...
    [DllImport("__Internal")]
    extern void _JSStorageSet(string key, int value);
    [DllImport("__Internal")]
    static extern int _JSStorageGet(string key, int fallbackValue);

    // ... and rather expose them like this so you can wrap them in preprocessors and provide alternative implementations
    public static void SetInt(string key, int value)
    {
        _JSStorageSet(key, value);
    }

    public static int GetInt(string key, int fallback = 0)
    {
        return _JSStorageGet(key, fallbackValue);
    }
}

and

mergeInto(LibraryManager.library, {
  _JSStorageSet: function (keyPtr, value) {

    // note that what you get is only pointers to the strings in HEAP8 buffer
    // => you first need to actually read them out!
    var keyString = UTF8ToString(keyPtr);

    console.log(value);

    // Deal with api
  },
  _JSStorageGet: function (keyPtr, fallbackValue) {

    // you first need to actually read them out!
    var keyString = UTF8ToString(keyPtr);

    // Deal with api and callback 

    //return fallbackValue;
    return 42;
  }
);

If you really want/require string then the return is a bit more complicated. See e.g. this answer for how to directly return string from JavaScript.

There is one disadvantage though: You allocate a buffer and don't free it!

You could have a dirty workaround for this by just having a setTimeout calling _free the buffer pointer after an amount of time that ensures that c# has definitely received it - a couple of seconds.

Personally I'm not a fan of that approach though so I solved this once using a bit more complex approach:

  • You can and should pass a Callback Pointer
    • There is a catch: The callback method is static so in order to call the correct callback I used a dictionary and a simple unique request counter
  • And then simply pass along a unique ID for each request and use a Dictionary to map the request ID back to the callback once you receive the result on c# side.

In your case this could look like e.g.

public static class APIController
{
    // Delegate type for the callback to adjust signature in a single place
    private delegate void RequestCallback(uint requestID, string result);

    // stores active requests and according callback action
    private static readonly Dictionary<uint, Action<string>> requestCallbacks = new();

    // unique request counter - is simply increased for each new request
    // (note that "uint" is built-in supported by JavaScript but not "long" ;) )
    private static uint currentID;

    [DllImport("__Internal")]
    static extern void _JSStorageSet(string key, string value);
    [DllImport("__Internal")]
    static extern void _JSStorageGet(string key, RequestCallback callback);

    // MonoPInvokeCallback handles the marshalling of the received values and pointers 
    // into the expected signature types
    [MonoPInvokeCallback(typeof(RequestCallback))]
    public static void OnGetCompleted(uint requestID, string result)
    {
        if(requestCallbacks.Remove(requestID, out var callback))
        {
            callback?.Invoke(result);
        }
    }

    public static void SetString(string key, string value)
    {
        _JSStorageSet(key, value);
    }

    public static void GetString(string key, Action<string> onResult, string fallbackValue = string.Empty)
    {
        requestCallbacks.Add(currentID, onResult);

        _JSStorageGet(currentID, key, OnGetCompleted, fallbackValue);

        currentID++;
    }
}

and

mergeInto(LibraryManager.library, {
  _JSStorageSet: function (keyPtr, valuePtr) {
    // note that what you get is only pointers to the strings in HEAP8 buffer
    // => you first need to actually read them out!
    var keyString = UTF8ToString(keyPtr);
    var valueString = UTF8ToString(valuePtr);

    // Deal with api

    
    console.log(value);
  },

  _JSStorageGet: async function (requestID, keyPtr, callbackPtr, fallbackValuePtr) {
    // as before the string you need to read out first
    var keyString = UTF8ToString(keyPtr);
    var fallbackValueString = UTF8ToString(fallbackValuePtr);

    // Deal with api and callback 

    var result = "Hello World!";
    //var result = fallbackValueString;

    // pack the string into a HEAP8 buffer
    var resultPtr = stringToNewUTF8(result);
    // invoke the passed callback
    // vii => matches signature Void (int, int)
    // uint is treated at int at this point and the string is an int pointer
    // on c# side the [MonoPInvokeCallback(typeof(RequestCallback))] will handle the marshelling of both types
    // back to the expected uint and string
    {{{ makeDynCall('vii', 'callbackPtr') }}} (requestID, resultPtr);
    // release the allocated buffer memory
    _free(resultPtr);
  }
);

I hope that gives you an idea and good starting point - interop coding can grow as complex as you like ^^

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks you very much for the reply. The int option looks better and simpler. Faced with the following problem. The API code inside the _JS StorageGet function is executed asynchronously and fineshed after calling return value, i.e. _JS Storage Get returns an empty value. How can I wait for the value to be received in the example, or any other solution?
Then again I would go for the callback way at the bottom just using int instead of string there
If I use Callback in JavaScript, I get the same problem but in Unity. Callback function return value asynchronously too late. How I can put here result of callback instead of PlayerPrefs function: public int LoadLevelStarsCount(int level) { return PlayerPrefs.GetInt(GetLevelKey(level), 0); // How to put there result of callback function instead of PlayerPrefs }
The way I showed you in the lower section of my answer .. you use a callback instead of directly return a value ... You could also use UniTask and directly use async - await pattern in WebGL but that's a bit to far for the scope of this question
Good afternoon. Thank you for you explanation. I think i don't explain my question correctly. Try again, in next topic:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.