Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 1 addition & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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.<anonymous> (/home/tstableford/projects/wasmoon/dist/index.js:142:22)
at Generator.throw (<anonymous>)
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
```
21 changes: 16 additions & 5 deletions src/type-extensions/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -211,11 +211,22 @@ class FunctionTypeExtension extends TypeExtension<FunctionType, FunctionDecorati
callThread.setTimeout(Date.now() + this.options.functionTimeout)
}

const status: LuaReturn = callThread.lua.lua_pcallk(callThread.address, args.length, 1, 0, 0, null)
if (status === LuaReturn.Yield) {
throw new Error('cannot yield in callbacks from javascript')
const resumeResult: LuaResumeResult = callThread.resume(args.length)
if (resumeResult.result === LuaReturn.Yield) {
return new Promise((r, c) => {
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)
Expand Down
11 changes: 3 additions & 8 deletions test/engine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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 () => {
Expand Down
21 changes: 21 additions & 0 deletions test/promises.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EventEmitter } from 'events'
import { expect } from 'chai'
import { getEngine, tick } from './utils.js'
import { mock } from 'node:test'
Expand Down Expand Up @@ -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)))
Expand Down