konsoletyper / teavm

Compiles Java bytecode to JavaScript, WebAssembly and C
https://teavm.org
Apache License 2.0
2.55k stars 260 forks source link

JSO: add interface for Promise #884

Closed tryone144 closed 4 months ago

tryone144 commented 4 months ago

This adds the jso.core.JSPromise interface to interact with native JavaScript Promises.

I've added this to return Promises (and async computation results) back to the JavaScript context. Is there a built in way for asynchronous execution we can await on the JavaScript side? I like the async/await syntax with Promises more like completion callbacks (which are already possible in TeaVM).

Usage example: I use this in my project by spawning a new thread and calling the `resolve` and `reject` methods after the inner thread has finished (or failed). The inner runner could be a simple function as well. ```java import org.teavm.jso.JSObject; import org.teavm.jso.core.JSFunction; import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSPromise; import org.teavm.jso.core.JSString; /** * Thread executor that returns a JavaScript Promise which resolves/rejects depending on the execution result. */ static class ThreadExecutor extends Thread implements JSPromise.Executor { private Thread _thread; private int _timeout; private JSFunction _resolve; private JSFunction _reject; public ThreadExecutor(Thread thread, int timeout) { _thread = thread; _timeout = timeout; } /** Thread runner entry point */ @Override public void run() { _thread.start(); try { _thread.join(); } catch (InterruptedException e) { e.printStackTrace(); if (_reject != null && !JSObjects.isUndefined(_reject)) { _reject.call(this, JSString.valueOf("Interrupted")); } } catch (Exception e) { if (_reject != null && !JSObjects.isUndefined(_reject)) { _reject.call(this, JSString.valueOf(e.toString())); } } finally { if (_resolve != null && !JSObjects.isUndefined(_resolve)) { _resolve.call(this); } } } /** Promise runner entry point */ @Override public void onExecute(JSFunction resolveFunc, JSFunction rejectFunc) { _resolve = resolveFunc; _reject = rejectFunc; this.start(); } } /** Use it in a function */ public JSPromise execute(String code, JSNumber timeoutMs) { Thread runner = new Thread(() -> {}); // create Thread that does something with `code` int timeout = JSObjects.isUndefined(timeoutMs) ? 0 : timeoutMs.intValue(); ThreadExecutor executor = new ThreadExecutor(runner, timeout); JSPromise promise = JSPromise.create(executor); return promise; } ``` I think, this has quite a bit of boilerplate code, that would be the same for most use-cases. Maybe this can be abstracted away as well as part of the API?
tryone144 commented 4 months ago

Adding generic type parameters made this class quite a bit more "complex". Is there a default location for JSO specific tests?

konsoletyper commented 4 months ago

No, never even tried to test JSO APIs.

konsoletyper commented 4 months ago

Don't see any purpose in that. With JSO you don't provide any implementations, just declarations. So what really need to be tested here is JSO itself (whether annotations propertly recognized). JSO declarations don't have regression, so never seen any point to write automatic tests.

tryone144 commented 4 months ago

With JSO you don't provide any implementations, just declarations. …

Okay, makes sense. In this concrete case, this would mostly be to make sure my type parameters make sense. I'll push a WIP shortly.

konsoletyper commented 4 months ago

Thanks! BTW, how is your project with TeaVM going?

tryone144 commented 4 months ago

Thanks! BTW, how is your project with TeaVM going?

I have built a working prototype around july last year. Currently working on integrating the latest changes in TeaVM, polishing my changes to TeaVM and updating our build-system.

I will try replacing my hacky workaround for exporting Java functions and constants with the newly landed @JSExport annotations next. The new module generator is nicer than the one I had written. :smile:

Be prepared for a PR regarding the long-emulation layer in the JS backend. I found some bottlenecks (at least on Firefox) with the use of BigInts. Right now I am writing a benchmark sample to generate some baseline metrics in different environments.

konsoletyper commented 4 months ago

The new module generator is nicer than the one I had written.

Thanks. This is the very first prototype. I'm going to add various improvements eventually:

  1. Allow to overload exported methods by signature, when possible
  2. Allow to export constructors
  3. Support varargs
  4. Generate d.ts files
  5. Verify exports and produce human-readable error messages

Anyway, I'm glad to get any feedback from users regarding this issue and possible improvements.

aghasemi commented 3 months ago

Hi. Is this available in version 0.9.2?

konsoletyper commented 3 months ago

@aghasemi no, it's available in 0.10.0-dev-8 and will be available in 0.10.0

aghasemi commented 2 months ago

Hi. I know the answer is very likely "It's done when it's done", but still, is there an estimate of when 0.10.0 will be out? Given how many Javascript APIs and libraries use Promises, this is a big change.

konsoletyper commented 2 months ago

@aghasemi I still don't undestand why it's a show-stopper for you. There's no Promise definition in 0.9.0, but what prevents you from defining your own?

aghasemi commented 2 months ago

Lack of expertise? :)

It seems non-intuitive for me to convert between a Java Future and a JS Promise, unlike the case of the data types. Can you give me some hints to start with?

Currently what I do as a workaround is to to write a call back in Javascript which does a postMessage and then listen to that message in TeaVM Java code.

konsoletyper commented 2 months ago

It seems non-intuitive for me to convert between a Java Future and a JS Promise, unlike the case of the data types. Can you give me some hints to start with?

Sorry, I don't fully understand what you want? Just JS promise declarations or some connection of Future interface? This PR addresses only former.

Currently what I do as a workaround is to to write a call back in Javascript which does a postMessage and then listen to that message in TeaVM Java code.

How is it related to Promise class?

tryone144 commented 2 months ago

It seems non-intuitive for me to convert between a Java Future and a JS Promise, unlike the case of the data types.

This PR only introduces an interface to interact with the Promise type in JavaScript. It shares its scope with the other JavaScript interop classes (JSString, JSNumber, JSArray, etc.) to allow interfacing with JavaScript methods that return Promises or expect a Promise as a parameter.

This does neither provide nor intend to provide an asynchronous executor for Java code and the Future interface. You might provide your own implementation using the JSPromise as a backend, though.

Can you give me some hints to start with?

If you can't update to a 0.10 snapshot release, you can get away with copying this implementation into your project (minus the recent changes) and using this class directly.

If you really need an executor and the Future interface in the classlib, you have to provide your own implementation in your project and reference it in a META-INF/teavm.properties file. Easiest way would be to put these classes/interfaces in their own package and adjust the map* and strip* entries accordingly to have them appear in their proper place in the classlib hierarchy.

Currently what I do as a workaround is to to write a call back in Javascript which does a postMessage and then listen to that message in TeaVM Java code.

What kind of problem are you trying to solve? Async execution in Java? :raised_eyebrow:

aghasemi commented 2 months ago

What kind of problem are you trying to solve? Async execution in Java? 🤨

Consider the following Javascript expression:

import('https://cdn.jsdelivr.net/npm/@xenova/transformers').then(async ({pipeline}) => {
                return await pipeline('sentiment-analysis', 'Xenova/bert-base-multilingual-uncased-sentiment').then(async pipe => {
                    return await pipe('It may not be that bad, actually!').then(out => {
                        console.log(JSON.stringify(out))
                        return out
                    })
                })

            })

If you evaluate it, the result is a Promise object, which when itself evaluated/executed, returns a JSON object (in this case [{label=3 stars, score=0.33625108003616333}]).

My "problem" is, how to get to that JSON object in my TeaVM Java code, as a Future object or not.

konsoletyper commented 2 months ago

@aghasemi you can get it as a Promise object. You don't necessarily need Promise declaration out-of-the box (honestly, the declarations in TeaVM are only for "quick start", and for real world scenarios you may need to declare your own bindings), but rather to write your own Promise "implementation". The key is that it's super easy, read documentation

aghasemi commented 2 months ago

Thanks for the hint. For future reference, the following program correctly works for the code snippet mentioned above:

package io.aghasemi;

import java.io.IOException;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject;
import org.teavm.jso.json.JSON;

public class Client {

    private static final String jsCode =  """
        import('https://cdn.jsdelivr.net/npm/@xenova/transformers').then(async ({pipeline}) => {
            return await pipeline('sentiment-analysis', 'Xenova/bert-base-multilingual-uncased-sentiment').then(async pipe => {
                return await pipe('It may not be that bad, actually!').then(out => {
                    console.log(JSON.stringify(out))
                    return out
                })
            })

        }).then(result => {
            handler(result)
        })
    """;

    @JSFunctor
    public interface PromiseHandler extends JSObject {
        void then(JSObject result);
    }

    @JSBody(params = { "handler" }, script = jsCode)
    static native void runPromiseCode(PromiseHandler handler);

    public static void main(String[] args) throws IOException {

        runPromiseCode(new PromiseHandler() {

            @Override
            public void then(JSObject result) {
                System.out.println("In Java code, the result is "+ JSON.stringify(result));
            }

        });

    }
}

Now, as the a rather minor issue, you see above that I had to stringify the returned JS object and likely later will have to again convert it to JSON in Java code to process it further. Can I directly get a usable (in Java) JSON object as result, or there is no way around it?

Thanks again.

konsoletyper commented 2 months ago

You can do with this JSObject whatever you want, it's just direct reference to real JavaScript object. For example, write a type declaration for this JSON object and cast it to required type. Or just declare this particular type

void then(JSObject result);

instead of JSObject. You can also use something like JSMapLike<JSObject>, but then you'll end up with lots of casting and stringly typing. BTW, there's nothing mystical about JSMapLike, it's simply declared as

public interface JSMapLike<T> extends JSObject {
    @JSIndexer
    T get(String key);

    @JSIndexer
    void set(String key, T object);
}
aghasemi commented 2 months ago

Amazing. many thanks.