ceifa / wasmoon

A real lua 5.4 VM with JS bindings made with webassembly
MIT License
498 stars 32 forks source link

Async functions #14

Closed timstableford closed 3 years ago

timstableford commented 3 years ago

I only noticed your Issue https://github.com/ceifa/wasmoon/issues/13 awhile creating this so that's a positive coincidence.

I originally tried to use Emscripten's Asyncify functionality but couldn't figure out a way without modifying Lua code. So what this does is if a callback returns a promise it calls lua_yieldk with a reference to the promise so it bubbles up to lua_resume. I did a lot of Googling about how to handle arbitrary references thinking there'd be an equivalent to napi_ref from Node but I stumbled on an Issue in their repo where they say to just keep a registry similar to how functions work. I also added a wrapper function around lua_resume that awaits promises and continues until the function is done yielding.

As a bonus to the above I've allowed pushValue to store JS values as references if the option is passed and getValue maps the references back to their JS objects.

Entirely tangentially to the promise handling I also added code to get current memory use and set a maximum. The custom allocator is currently always passed to lua_newstate, I'm happy to not do so if you'd prefer.

Please also let me know if you see a better way of doing Promise handling.

The next items you're likely to see from me are:

ceifa commented 3 years ago

Great work!

With this solution to promises, how can I call async functions without needing to create a new thread from JS? For example:

    const engine = await getEngine()

    engine.global.set('asyncCallback', async (input) => {
        return Promise.resolve(input * 2)
    })

    const value = engine.doString(`
        local resume = coroutine.wrap(function()
            print(asyncCallback(15)) -- ""
        end)
        print(resume(co)) -- 15
        print(resume(co)) -- ""
        -- coroutine is dead now
    `)

I expected to log a "30" somewhere, but it didn't.

timstableford commented 3 years ago

Afaik it's only possible to yield from a thread rather than the main state.

I'm not certain what's happening in your example. I'll do a bit of investigation. We could modify the doString and doFile helpers to create the new thread automatically perhaps?

Try and call it like this https://github.com/ceifa/wasmoon/blob/297e95ccdf8c53f59f4a56c376db574f6069e260/test/engine.test.js#L237

timstableford commented 3 years ago

I suspect with coroutine.wrap it doesn't know when the promise is resolved

ceifa commented 3 years ago

I was thinking to create an API like that:

local promise = asyncCallback(15)
promise:then(print) -- The same behaviour of JS then
promise:catch(error) -- The same behaviour of JS catch
local result = promise:await() -- Will only work inside a thread(coroutine), basically will yield while the promise is pending and then return the result value

What do you think?

ceifa commented 3 years ago

Answering your question, I don't think creating a new thread automatically it's a good approach because as a client I will probably expect the code to run on the main thread.

timstableford commented 3 years ago

I'm having trouble figuring out the flow of how it would work. If you only had a :then I think the Lua program would just exit early before the then resolves unless it's await as per your example. With catch it may be better to use Lua's built-in try/catch by letting users wrap the async calls in a pcall? I very much see where you're coming from by trying to expose JS functionality directly to it for flexibility I'm just not sure of the underlying details without somehow blocking the program ending like Node does while there's pending timers.

timstableford commented 3 years ago

Answering your question, I don't think creating a new thread automatically it's a good approach because as a client I will probably expect the code to run on the main thread.

I understand what you mean, that could make things rather confusing. I think it's the only way it will allow yielding but I can use lua_xmove to move the results from the thread to the calling state?

ceifa commented 3 years ago

I agree and I don't have concrete answers to your questions. :/ I'm going to merge because you did a great start for promises support and we can improve it later. Thanks again for contributing this! 👍

ceifa commented 3 years ago

@timstableford I think that there is nothing to do about the early return, this actually happens if you bind setTimeout, for example. I implemented a next(then is a keyword) function to see how it would be, take a look at #15.

Something we can do is create some API like await engine.global.waitUnresolvedPromises().