LiquidPlayer / LiquidCore

Node.js virtual machine for Android and iOS
MIT License
1.01k stars 128 forks source link

How do I run a JS function from a JS file in assets in Android #64

Closed ajilo297 closed 6 years ago

ajilo297 commented 6 years ago

Is it possible to run a JS function using this library if the .js file is stored in the res directory? It's not really clear for me from the docs

ericwlange commented 6 years ago

Yes.

I assume you are using LiquidCore as a raw JavaScript engine on Android? If you are using a MicroService, you can simply specify a resource URI (android.resource:// ...) instead of a network URL. If you are using the engine directly (bypassing the service API), then there are a number of ways to do it. But the easiest way is to load the file into a String using normal Java and call JSContext.evaluateScript(string). And then you can get and call the function. For example, if your js file is:

/** my file **/
function foo(a) {
    return a + 1;
}

And then in Java:

String code = ( ... load code from file into string ... );
JSContext context = new JSContext();
context.evaluateScript(code);
JSFunction jsFoo = context.property("foo").toObject().toFunction();
Integer aplus1 = jsFoo.call(null, 5).toNumber().intValue();
// aplus1 = 6
ajilo297 commented 6 years ago

Hello Eric,

I have been trying to use this method to work on a JS file that i have added to the Android resources directory.

String code = ( ... load code from file into string ... );
JSContext context = new Context();
context.evaluateScript(code);
JSFunction jsFoo = context.property("foo").toObject().toFunction();
Integer aplus1 = jsFoo.call(null, 5).toNumber().intValue();

I can confirm that this works. I have figured out how to call constructors, etc. But how do I handle if a function/constructor returns a promise?

ericwlange commented 6 years ago

A promise is just another JSObject. There isn't a nice Java abstraction for it, though. You would have to handle that yourself. For example:

JSObject promise = jsFoo.call(null, 5).toObject();
promise.property("then").toObject().toFunction().call(promise,
 new JSFunction(context, "then") {
    public void then(Integer value) {
        /* Do something with value */
    }
});

This is equivalent to the javascript:

var promise = jsFoo(5);
promise.then((value) => {
   /* Do something with value */
});

Note, in the example above, I am assuming your value will be an Integer. It doesn't have to be, it can be any simple data type. If you don't know the type in advance, use JSValue.

The one question I have, though, is how and when does your Javascript code resolve the promise? Most promises are handled asynchronously, which means they depend on setTimeout() or the async keyword which generate delayed microtasks. As you are using the raw Javascript engine, this functionality is not there by default. You would have to provide an implementation. For example, if it relies on setTimeout, you can do this at startup:

context.property("setTimeout", new JSFunction(context, "setTimeout") {
    void setTimeout(final JSFunction callback, final Integer millis, final JSValue ...args) {
        new Thread(new Runnable() {
            public void run(){
                Thread.sleep(millis);
                callback.apply(null, args);
            }
        }).start();
    };
});

This will polyfill setTimeout(). But again, it depends on what your implementation needs.

This is the advantage of using a MicroService. A micro service is a fully functional Node.js virtual machine that handles all of this for you. It runs in a single thread, handles async calls, microtasks, and thread safety. And you get the entire node runtime, which includes setTimeout as well as everything else you could want (short of the DOM).

(Note, none of the code above has been tested. It just came from memory, so it may be buggy.)

ajilo297 commented 6 years ago

I am actually trying to run Ether JS library. The official documentation is available here.

I am trying to create a Wallet using the fromBrainWallet constructor. It takes in an emailID, password and a callback function.

When I use your code, the promise object does not have a property called then.

I have absolutely no idea about how I should handle this.

This is the code I am using

JSFunction function = new JSFunction(context, "then"){
    public void then(JSObject object) {
        // i hope to get the wallet object here
    }
};
JSObject obj = wallet.property("fromBrainWallet")
        .toFunction()
        .call(null, email, password, function)
        .toObject(); // When running in debug mode, obj: "[object Promise]"

Like you mentioned, I too think that I might have to use a MicroService, but again I am not sure about how to do that.

ericwlange commented 6 years ago

The other thing you can do is run a node Process directly. I have verified this works:

public class PromiseTest {

    private int counter = 0;
    private Semaphore waitToFinish = new Semaphore(0);

    @Test
    public void testPromise()
    {
        final String code =
                "new Promise((resolve,reject)=>{" +
                "    console.log('Wait 1 second to resolve');" +
                "    setTimeout(resolve,1000);" +
                "})";

        Process.EventListener listener = new Process.EventListener() {
            @Override
            public void onProcessStart(final Process process, final JSContext context) {
                JSObject promise = context.evaluateScript(code).toObject();
                promise.property("then").toFunction()
                    .call(promise, new JSFunction(context, "then") {
                        public void then() {
                            android.util.Log.d("PromiseTest", "Calling then");
                            counter++;
                            assertEquals(2,counter);
                        }
                    }
                );
            }

            @Override
            public void onProcessExit(final Process process, int exitCode) {
                counter ++;
                waitToFinish.release();
            }

            @Override
            public void onProcessAboutToExit(Process process, int exitCode) {}

            @Override
            public void onProcessFailed(final Process process, Exception error) {
            }
        };

        counter++;
        Process process = new Process(
                InstrumentationRegistry.getContext(),
                "ProcessTest",
                Process.kMediaAccessPermissionsRW,
                listener
        );
        assertEquals(1, counter);
        android.util.Log.d("PromiseTest", "Got Process.");

        waitToFinish.acquireUninterruptibly();
        assertEquals(3, counter);
        android.util.Log.d("PromiseTest", "Process done");
    }

}

Logcat:

09-29 11:07:33.255 15958-15975/? I/TestRunner: run started: 1 tests
09-29 11:07:33.258 15958-15975/? I/TestRunner: started: testPromise(org.liquidplayer.node.PromiseTest)
09-29 11:07:33.280 15958-15975/? D/PromiseTest: Got Process.
09-29 11:07:33.441 15958-15979/? I/stdout: Wait 1 second to resolve
09-29 11:07:34.446 15958-15979/org.liquidplayer.node.test D/PromiseTest: Calling then
09-29 11:07:34.455 15958-15975/org.liquidplayer.node.test D/PromiseTest: Process done
09-29 11:07:34.455 15958-15975/org.liquidplayer.node.test I/TestRunner: finished: testPromise(org.liquidplayer.node.PromiseTest)
09-29 11:07:34.459 15958-15958/org.liquidplayer.node.test I/MonitoringInstr: Activities that are still in CREATED to STOPPED: 0
09-29 11:07:34.460 15958-15975/org.liquidplayer.node.test I/TestRunner: run finished: 1 tests, 0 failed, 0 ignored

A MicroService is an abstraction of the node Process. If you don't need or want to use the full service API, you can use Process directly. Simply, when you call the constructor, it asynchronously creates a node.js process and calls your onProcessStart listener when the process is ready. You can then use the Javascript API to run your code. This will run all of your JS in a single thread. The thread will stay alive until there are no more callbacks pending and then it will quit. This way, promises can be handled as expected. See the documentation for more info.