fix(lua): WASM memory access — resume coroutine через lua.global.get вместо doStringSync

При каждом tick'е tickScheduler делал lua.doStringSync(resume code),
что вызывало re-entrant WASM-крах memory access out of bounds после
нескольких task.wait() итераций.

Фикс: кешируем ссылку на __rbxl_resume_co при init и зовём её напрямую.
Это безопасный путь (не парсит код заново, не открывает вложенный
Lua-state поверх существующего).
This commit is contained in:
min 2026-06-08 11:50:22 +03:00
parent 8ac2637615
commit 0d7224a2b8

View File

@ -734,10 +734,11 @@ export function registerRobloxShim(lua, opts) {
});
});
// Лагалейн Lua-код: определяем task.wait, wait, и обёртку для скрипта.
// Этот код выполняется ВНУТРИ Lua VM.
// Lua-prelude: task.wait через coroutine.yield + готовая resume-функция.
// Главное: __rbxl_resume_co определена в Lua и вызывается из JS через
// lua.global.get('__rbxl_resume_co') — это безопаснее чем doStringSync
// потому что не парсит код заново и не создаёт re-entrant проблем.
lua.doStringSync(`
-- task.wait(sec) coroutine.yield(sec); main-loop вернёт через delay sec
local function rbx_wait(sec)
sec = sec or 0
coroutine.yield(sec)
@ -749,7 +750,23 @@ export function registerRobloxShim(lua, opts) {
task = { wait = rbx_wait }
end
wait = rbx_wait
-- Вызывается из JS-tickScheduler:
-- возвращает next-delay (number) если co yield'нулся ещё раз,
-- или nil если co завершился / умер.
function __rbxl_resume_co(co)
if not co or coroutine.status(co) ~= 'suspended' then return nil end
local ok, ret = coroutine.resume(co)
if not ok then
return false, tostring(ret)
end
if coroutine.status(co) == 'dead' then return nil end
if type(ret) == 'number' then return ret end
return 0
end
`);
// Достаём ссылку на Lua-функцию один раз; вызовы безопасны (не doStringSync)
const luaResumeCo = lua.global.get('__rbxl_resume_co');
// === Setter Part-свойств (Position/Size/Color/...) ===
// Юзер пишет: part.Position = Vector3.new(0, 10, 0)
@ -805,39 +822,32 @@ export function registerRobloxShim(lua, opts) {
}
}
// 2. Резюм coroutine'ов которые task.wait()
// Скрипт-coroutine при первом запуске yield'ит с delay (от task.wait).
// Мы регистрируем delay через __rbxl_schedule_resume? Нет — проще:
// отслеживаем последний yield-результат через coroutine.resume → возвращает
// delay. После init скрипта проверим всех coroutines: если status==suspended,
// и им пора — резюмируем.
for (const [coId, co] of coroutines) {
let entry = waitingCoros.find(w => w.coId === coId);
if (!entry) {
// Первый раз видим этот co — нужно вытащить delay из yield-результата.
// Это сделано в _onYield ниже.
continue;
// Используем lua.global.get-кешированную __rbxl_resume_co функцию —
// безопаснее чем doStringSync (не re-entrant в WASM).
const dueCoros = [];
for (let i = waitingCoros.length - 1; i >= 0; i--) {
if (waitingCoros[i].wakeAt <= now) {
dueCoros.push(waitingCoros[i]);
waitingCoros.splice(i, 1);
}
if (entry.wakeAt > now) continue;
// Время резюмить
waitingCoros.splice(waitingCoros.indexOf(entry), 1);
}
for (const entry of dueCoros) {
const co = coroutines.get(entry.coId);
if (!co) continue;
try {
const code = `
local co = __rbxl_get_co(${JSON.stringify(coId)})
if co and coroutine.status(co) == 'suspended' then
local ok, ret = coroutine.resume(co)
if not ok then
__rbxl_send_error(${JSON.stringify(coId)}, tostring(ret))
__rbxl_unregister_coroutine(${JSON.stringify(coId)})
elseif type(ret) == 'number' then
__rbxl_schedule_resume(${JSON.stringify(coId)}, ret)
elseif coroutine.status(co) == 'dead' then
__rbxl_unregister_coroutine(${JSON.stringify(coId)})
end
end
`;
lua.doStringSync(code);
const result = luaResumeCo(co);
// result: number (next delay), nil (done), false+errStr (failed)
if (result === null || result === undefined) {
coroutines.delete(entry.coId);
} else if (typeof result === 'number') {
waitingCoros.push({
coId: entry.coId,
wakeAt: SCHEDULER.now() + result * 1000,
});
}
} catch (e) {
send('log', { level: 'error', text: `[coroutine resume ${coId}] ${e?.message || e}` });
send('log', { level: 'error', text: `[coroutine ${entry.coId}] ${e?.message || e}` });
coroutines.delete(entry.coId);
}
}
},