Davidobot / love.js

LÖVE ported to the web using Emscripten, updated to the latest Emscripten and LÖVE (v11.5)
MIT License
624 stars 28 forks source link

Error when coroutines are wrapped with xpcall #54

Open idbrii opened 2 years ago

idbrii commented 2 years ago

lovejs errors with "attempt to yield across metamethod/C-call boundary" when you try to yield in a coroutine inside xpcall. love2d native (11.3) runs the same code without issue. I'm not sure why the behaviour is different and this use case doesn't seem super common, but it tripped me up so I thought I'd post an issue.

I ran into this because I was using batteries.async which wraps all coroutines in xpcall to capture the immediate callstack for errors instead of where the one at coroutine.resume.


An example of the issue (tested on 9e07fa0b1c50bb1c1132a843b5ede661f141ed53 using pfirsich/makelove to package compat):

local color = {
    purple = {0.25, 0.09, 0.28, 1},
    white = {0.89, 0.91, 0.90, 1},
}
local ball = {
    x = 100,
    y = 100,
    r = 20,
}
local input = {
    coro = 'v',
}
local S = {}

local function coro_fn()
    S.async_running = true
    for i=1,100 do
        color.purple[4] = i / 100
        coroutine.yield()
        color.purple[4] = ten + 1
    end
    color.purple[4] = 1
    S.async_running = false
    return true
end
local function safety_wrap(f)
    return function(...)
        local results = {xpcall(f, debug.traceback, ...)}
        local success = table.remove(results, 1)
        if not success then
            error(table.remove(results, 1))
        end
        return unpack(results)
    end
end

function love.update(dt)
    if S.coro then
        local success, result = coroutine.resume(S.coro)
        if not success then
            error(result)
        elseif result then
            print("coroutine cleared")
            S.coro = nil
        end
    end

    if not S.async_running then
        if love.keyboard.isDown(input.coro) then
            S.coro = coroutine.create((coro_fn))
            print("coroutine started")
        end
    end
end

function love.draw()
    love.graphics.setColor(color.purple)
    love.graphics.circle("fill", ball.x, ball.y, ball.r)

    love.graphics.setColor(color.white)
    love.graphics.printf("coro running: ".. tostring(S.coro or "no"), 5,5, 2000, "left")
    local str = "To start a coroutine:"
    for key,val in pairs(input) do
        str = ("%s\n%s: %s"):format(str, key, val)
    end
    love.graphics.printf(str, 5,25, 200, "left")
end

Pressing v to start the coroutine in lovejs produces this error:

Error: main.lua:47: main.lua:37: attempt to yield across metamethod/C-call boundary
love.js:9 stack traceback:
love.js:9   [C]: in function 'yield'
love.js:9   main.lua:26: in function <main.lua:22>
love.js:9   [C]: in function 'xpcall'
love.js:9   main.lua:34: in function <main.lua:33>
love.js:9 stack traceback:
love.js:9   [string "boot.lua"]:777: in function <[string "boot.lua"]:773>
love.js:9   [C]: in function 'error'
love.js:9   main.lua:47: in function 'update'
love.js:9   [string "boot.lua"]:612: in function <[string "boot.lua"]:594>
love.js:9   [C]: in function 'xpcall'