diff --git a/README.md b/README.md index a0f63e9..cca113d 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ npm test # ensure everything it's working fine ### Promises -Promises can be await'd from Lua with some caveats detailed in the below section. To await a Promise call `:await()` on it which will yield the Lua execution until the promise completes. +Promises can be await'd from Lua. To await a Promise call `:await()` on it which will yield the Lua execution until the promise completes. ```js const { LuaFactory } = require('wasmoon') @@ -206,61 +206,3 @@ try { lua.global.close() } ``` - -### Async/Await - -It's not possible to await in a callback from JS into Lua. This is a limitation of Lua but there are some workarounds. It can also be encountered when yielding at the top-level of a file. An example where you might encounter this is a snippet like this: - -```js -local res = sleep(1):next(function () - sleep(10):await() - return 15 -end) -print("res", res:await()) -``` - -Which will throw an error like this: - -``` -Error: Lua Error(ErrorRun/2): cannot resume dead coroutine - at Thread.assertOk (/home/tstableford/projects/wasmoon/dist/index.js:409:23) - at Thread. (/home/tstableford/projects/wasmoon/dist/index.js:142:22) - at Generator.throw () - at rejected (/home/tstableford/projects/wasmoon/dist/index.js:26:69) -``` - -Or like this: - -``` -attempt to yield across a C-call boundary -``` - -You can workaround this by doing something like below: - -```lua -function async(callback) - return function(...) - local co = coroutine.create(callback) - local safe, result = coroutine.resume(co, ...) - - return Promise.create(function(resolve, reject) - local function step() - if coroutine.status(co) == "dead" then - local send = safe and resolve or reject - return send(result) - end - - safe, result = coroutine.resume(co) - - if safe and result == Promise.resolve(result) then - result:finally(step) - else - step() - end - end - - result:finally(step) - end) - end -end -``` diff --git a/src/type-extensions/function.ts b/src/type-extensions/function.ts index 753ee90..6e94d2b 100644 --- a/src/type-extensions/function.ts +++ b/src/type-extensions/function.ts @@ -4,7 +4,7 @@ import MultiReturn from '../multireturn' import RawResult from '../raw-result' import Thread from '../thread' import TypeExtension from '../type-extension' -import { LUA_REGISTRYINDEX, LuaReturn, LuaState, LuaType, PointerSize } from '../types' +import { LUA_REGISTRYINDEX, LuaResumeResult, LuaReturn, LuaState, LuaType, PointerSize } from '../types' export interface FunctionDecoration extends BaseDecorationOptions { receiveArgsQuantity?: boolean @@ -211,11 +211,22 @@ class FunctionTypeExtension extends TypeExtension { + callThread + .run(0) + .then(() => { + if (callThread.getTop() > 0) { + r(callThread.getValue(-1)) + return + } + r(undefined) + }) + .catch(c) + }) } - callThread.assertOk(status) + callThread.assertOk(resumeResult.result) if (callThread.getTop() > 0) { return callThread.getValue(-1) diff --git a/test/engine.test.js b/test/engine.test.js index 23e6361..1eeac35 100644 --- a/test/engine.test.js +++ b/test/engine.test.js @@ -717,10 +717,7 @@ describe('Engine', () => { expect(res).to.be.equal('1689031554550') }) - it('yielding in a JS callback into Lua does not break lua state', async () => { - // When yielding within a callback the error 'attempt to yield across a C-call boundary'. - // This test just checks that throwing that error still allows the lua global to be - // re-used and doesn't cause JS to abort or some nonsense. + it('yielding in a JS callback into Lua should succeed', async () => { const engine = await getEngine() const testEmitter = new EventEmitter() engine.global.set('yield', () => new Promise((resolve) => testEmitter.once('resolve', resolve))) @@ -729,13 +726,11 @@ describe('Engine', () => { coroutine.yield() return 15 end) - print("res", res:await()) + return res:await() `) testEmitter.emit('resolve') - await expect(resPromise).to.eventually.be.rejectedWith('Error: attempt to yield across a C-call boundary') - - expect(await engine.doString(`return 42`)).to.equal(42) + expect(await resPromise).to.equal(15) }) it('forced yield within JS callback from Lua doesnt cause vm to crash', async () => { diff --git a/test/promises.test.js b/test/promises.test.js index d7b1d60..734c9bb 100644 --- a/test/promises.test.js +++ b/test/promises.test.js @@ -1,3 +1,4 @@ +import { EventEmitter } from 'events' import { expect } from 'chai' import { getEngine, tick } from './utils.js' import { mock } from 'node:test' @@ -146,6 +147,26 @@ describe('Promises', () => { expect(await asyncFunctionPromise).to.be.eql([50]) }) + it('await in a Lua function called from a JS callback should succeed', async () => { + const engine = await getEngine() + const testEmitter = new EventEmitter() + const promiseEmitter = new EventEmitter() + engine.global.set('promise', new Promise((resolve) => promiseEmitter.once('resolve', resolve))) + engine.global.set('yield', () => new Promise((resolve) => testEmitter.once('resolve', resolve))) + const resPromise = engine.doString(` + local res = yield():next(function () + promise:await() + return 20 + end) + return res:await() + `) + + testEmitter.emit('resolve') + setTimeout(() => promiseEmitter.emit('resolve'), 50) + + expect(await resPromise).to.equal(20) + }) + it('run thread with async calls and yields should succeed', async () => { const engine = await getEngine() engine.global.set('sleep', (input) => new Promise((resolve) => setTimeout(resolve, input)))