From f514ef1b53f3fc3eb214797afc1dfcccd42ac270 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 13 Mar 2025 16:11:23 +0100 Subject: [PATCH] Lua: support passing Lua function callbacks to native JS functions --- cmd/plug_run.ts | 2 +- common/space_lua/eval.test.ts | 26 +++++++++++++++++++++---- common/space_lua/eval.ts | 4 ++-- common/space_lua/runtime.ts | 29 +++++++++++++++++----------- common/space_lua/stdlib/js.ts | 6 +++--- common/space_lua/stdlib/space_lua.ts | 3 ++- common/syscalls/command.ts | 2 ++ common/syscalls/event.ts | 1 + common/syscalls/index.ts | 2 +- common/syscalls/lua.ts | 2 +- lib/plugos/syscalls/datastore.ts | 2 +- web/cm_plugins/lua_directive.ts | 1 + 12 files changed, 55 insertions(+), 25 deletions(-) diff --git a/cmd/plug_run.ts b/cmd/plug_run.ts index 611f137f..dec4748a 100644 --- a/cmd/plug_run.ts +++ b/cmd/plug_run.ts @@ -7,7 +7,7 @@ import { DiskSpacePrimitives } from "$common/spaces/disk_space_primitives.ts"; import { ServerSystem } from "../server/server_system.ts"; import { AssetBundlePlugSpacePrimitives } from "$common/spaces/asset_bundle_space_primitives.ts"; import { LocalShell } from "../server/shell_backend.ts"; -import { Hono } from "hono/mod.ts"; +import { Hono } from "hono"; import { DataStore } from "$lib/data/datastore.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; import { EventHook } from "../common/hooks/event.ts"; diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index 15220ce4..389ad717 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -12,6 +12,8 @@ import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts"; import { evalExpression, evalStatement } from "./eval.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; +const sf = LuaStackFrame.lostFrame; + function evalExpr(s: string, e = new LuaEnv(), sf?: LuaStackFrame): any { const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement; sf = sf || new LuaStackFrame(e, node.ctx); @@ -57,15 +59,15 @@ Deno.test("Evaluator test", async () => { assertEquals(tbl.get(1), 3); assertEquals(tbl.get(2), 1); assertEquals(tbl.get(3), 2); - assertEquals(luaValueToJS(tbl), [3, 1, 2]); + assertEquals(luaValueToJS(tbl, sf), [3, 1, 2]); - assertEquals(luaValueToJS(evalExpr(`{name=test("Zef"), age=100}`, env)), { + assertEquals(luaValueToJS(evalExpr(`{name=test("Zef"), age=100}`, env), sf), { name: "Zef", age: 100, }); assertEquals( - luaValueToJS(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)), + luaValueToJS(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env), sf), { name: "Zef", age: 100, @@ -120,7 +122,7 @@ Deno.test("Statement evaluation", async () => { const env3 = new LuaEnv(); await evalBlock(`tbl = {1, 2, 3}`, env3); await evalBlock(`tbl[1] = 3`, env3); - assertEquals(luaValueToJS(env3.get("tbl")), [3, 2, 3]); + assertEquals(luaValueToJS(env3.get("tbl"), sf), [3, 2, 3]); await evalBlock("tbl.name = 'Zef'", env3); assertEquals(env3.get("tbl").get("name"), "Zef"); await evalBlock(`tbl[2] = {age=10}`, env3); @@ -283,6 +285,22 @@ Deno.test("Statement evaluation", async () => { end`, luaBuildStandardEnv(), ); + + // Passing a Lua function as callback to a JS function + const env12 = new LuaEnv(); + env12.set( + "runMe", + new LuaNativeJSFunction((fn) => { + return fn("Lua"); + }), + ); + assertEquals( + "Hello from Lua", + await evalExpr( + `runMe(function(name) return "Hello from " .. name end)`, + env12, + ), + ); }); Deno.test("Thread local _CTX", async () => { diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 2907b559..07ec3b71 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -300,11 +300,11 @@ export function evalExpression( sf.withCtx(e.ctx), ); } - collection = luaValueToJS(collection); + collection = luaValueToJS(collection, sf); // Check if collection is a queryable collection if (!collection.query) { // If not, try to convert it to JS and see if it's an array - collection = await luaValueToJS(collection); + collection = await luaValueToJS(collection, sf); if (!Array.isArray(collection)) { throw new LuaRuntimeError( "Collection does not support query", diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 3854efc6..3a5edbed 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -87,7 +87,7 @@ export class LuaEnv implements ILuaSettable, ILuaGettable { if (omitKeys.includes(key)) { continue; } - result[key] = luaValueToJS(this.get(key)); + result[key] = luaValueToJS(this.get(key), LuaStackFrame.lostFrame); } return result; } @@ -283,8 +283,8 @@ export class LuaNativeJSFunction implements ILuaFunction { } // Performs automatic conversion between Lua and JS values - call(_sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue { - const result = this.fn(...args.map(luaValueToJS)); + call(sf: LuaStackFrame, ...args: LuaValue[]): Promise | LuaValue { + const result = this.fn(...args.map((v) => luaValueToJS(v, sf))); if (result instanceof Promise) { return result.then(jsToLuaValue); } else { @@ -453,13 +453,13 @@ export class LuaTable implements ILuaSettable, ILuaGettable { toJSObject(): Record { const result: Record = {}; for (const key of this.keys()) { - result[key] = luaValueToJS(this.get(key)); + result[key] = luaValueToJS(this.get(key), LuaStackFrame.lostFrame); } return result; } toJSArray(): any[] { - return this.arrayPart.map(luaValueToJS); + return this.arrayPart.map((v) => luaValueToJS(v, LuaStackFrame.lostFrame)); } toJS(): Record | any[] { @@ -638,7 +638,9 @@ export function luaCall( ); } if (typeof fn === "function") { - const jsArgs = args.map(luaValueToJS); + const jsArgs = args.map((v) => + luaValueToJS(v, sf || LuaStackFrame.lostFrame) + ); // Native JS function return fn(...jsArgs); } @@ -847,7 +849,7 @@ export function luaToString( export function jsToLuaValue(value: any): any { if (value instanceof Promise) { - return value.then(luaValueToJS); + return value.then(jsToLuaValue); } if (value instanceof LuaTable) { return value; @@ -884,15 +886,20 @@ export function jsToLuaValue(value: any): any { } // Inverse of jsToLuaValue -export function luaValueToJS(value: any): any { +export function luaValueToJS(value: any, sf: LuaStackFrame): any { if (value instanceof Promise) { - return value.then(luaValueToJS); + return value.then((v) => luaValueToJS(v, sf)); } if (value instanceof LuaTable) { return value.toJS(); - } else if (value instanceof LuaNativeJSFunction) { + } else if ( + value instanceof LuaNativeJSFunction || value instanceof LuaFunction || + value instanceof LuaBuiltinFunction + ) { return (...args: any[]) => { - return jsToLuaValue(value.fn(...args.map(luaValueToJS))); + return jsToLuaValue( + value.call(sf, ...args.map((v) => luaValueToJS(v, sf))), + ); }; } else { return value; diff --git a/common/space_lua/stdlib/js.ts b/common/space_lua/stdlib/js.ts index 1b96412a..d0caf673 100644 --- a/common/space_lua/stdlib/js.ts +++ b/common/space_lua/stdlib/js.ts @@ -13,9 +13,9 @@ export const jsApi = new LuaTable({ * @returns The new instance. */ new: new LuaBuiltinFunction( - (_sf, constructorFn: any, ...args) => { + (sf, constructorFn: any, ...args) => { return new constructorFn( - ...args.map(luaValueToJS), + ...args.map((v) => luaValueToJS(v, sf)), ); }, ), @@ -53,7 +53,7 @@ export const jsApi = new LuaTable({ * @param val - The Lua value to convert. * @returns The JavaScript value. */ - tojs: new LuaBuiltinFunction((_sf, val) => luaValueToJS(val)), + tojs: new LuaBuiltinFunction((sf, val) => luaValueToJS(val, sf)), /** * Logs a message to the console. * @param args - The arguments to log. diff --git a/common/space_lua/stdlib/space_lua.ts b/common/space_lua/stdlib/space_lua.ts index 0c221182..9ba4722f 100644 --- a/common/space_lua/stdlib/space_lua.ts +++ b/common/space_lua/stdlib/space_lua.ts @@ -85,6 +85,7 @@ export async function interpolateLuaString( const env = createAugmentedEnv(sf, envAugmentation); const luaResult = luaValueToJS( await evalExpression(parsedExpr, env, sf), + sf, ); result += luaToString(luaResult); } catch (e: any) { @@ -124,7 +125,7 @@ export const spaceluaApi = new LuaTable({ evalExpression: new LuaBuiltinFunction( async (sf, parsedExpr: LuaExpression, envAugmentation?: LuaTable) => { const env = createAugmentedEnv(sf, envAugmentation); - return luaValueToJS(await evalExpression(parsedExpr, env, sf)); + return luaValueToJS(await evalExpression(parsedExpr, env, sf), sf); }, ), /** diff --git a/common/syscalls/command.ts b/common/syscalls/command.ts index 744d5b96..a2b20502 100644 --- a/common/syscalls/command.ts +++ b/common/syscalls/command.ts @@ -39,6 +39,7 @@ export function commandSyscalls( try { return luaValueToJS( await luaCall(def.run, args.map(jsToLuaValue), {}, sf), + sf, ); } catch (e: any) { await handleLuaError(e, commonSystem.system); @@ -61,6 +62,7 @@ export function commandSyscalls( try { return luaValueToJS( await luaCall(def.run, args.map(jsToLuaValue), {}, sf), + sf, ); } catch (e: any) { await handleLuaError(e, commonSystem.system); diff --git a/common/syscalls/event.ts b/common/syscalls/event.ts index baad7f91..35f85e42 100644 --- a/common/syscalls/event.ts +++ b/common/syscalls/event.ts @@ -37,6 +37,7 @@ export function eventListenerSyscalls( try { const val = luaValueToJS( await luaCall(def.run, args.map(jsToLuaValue), {}, sf), + sf, ); return val; } catch (e: any) { diff --git a/common/syscalls/index.ts b/common/syscalls/index.ts index 87458c33..76243475 100644 --- a/common/syscalls/index.ts +++ b/common/syscalls/index.ts @@ -35,7 +35,7 @@ export function indexSyscalls(commonSystem: CommonSystem): SysCallMapping { // We don't want to include the global environment in the serialized value value = value.toJSON(["_GLOBAL"]); } - const jsonValue = await luaValueToJS(value); + const jsonValue = await luaValueToJS(value, sf); // Ensure this is JSON serializable JSON.stringify(jsonValue); scopedVariables[v] = jsonValue; diff --git a/common/syscalls/lua.ts b/common/syscalls/lua.ts index 51ed6728..e5703ef6 100644 --- a/common/syscalls/lua.ts +++ b/common/syscalls/lua.ts @@ -34,7 +34,7 @@ export function luaSyscalls(commonSystem: CommonSystem): SysCallMapping { commonSystem.spaceLuaEnv.env, sf, ); - const jsResult = luaValueToJS(luaResult); + const jsResult = luaValueToJS(luaResult, sf); if (isSendable(jsResult)) { return jsResult; } else { diff --git a/lib/plugos/syscalls/datastore.ts b/lib/plugos/syscalls/datastore.ts index 582c1f12..d6056ddc 100644 --- a/lib/plugos/syscalls/datastore.ts +++ b/lib/plugos/syscalls/datastore.ts @@ -60,7 +60,7 @@ export function dataStoreReadSyscalls( query, env, sf, - )).map((item) => luaValueToJS(item)); + )).map((item) => luaValueToJS(item, sf)); }, "datastore.listFunctions": (): string[] => { diff --git a/web/cm_plugins/lua_directive.ts b/web/cm_plugins/lua_directive.ts index 761b5092..45699634 100644 --- a/web/cm_plugins/lua_directive.ts +++ b/web/cm_plugins/lua_directive.ts @@ -111,6 +111,7 @@ export function luaDirectivePlugin(client: Client) { threadLocalizedEnv, sf, ), + sf, ); // console.log("Result:", result); return result;