ceifa / wasmoon

A real lua 5.4 VM with JS bindings made with webassembly
MIT License
462 stars 27 forks source link

Can't use null in a table #39

Closed timstableford closed 8 months ago

timstableford commented 3 years ago

If a JS API explicitly needs the null value within an object it's not currently possible.

Eg

jsFunc({
  a = 10,
  b = nil
})

Will generate a JS object like this:

{ a: 10 }

That's because nil works far more like JS's undefined than null.

I think the best solution is adding a null type extension and setting the global "null" to this type extension allowing it to be used like a primitive.

ceifa commented 3 years ago

Indeed, and it should be under injectObjects flag

Leka74 commented 1 year ago

Is there a workaround for this?

tims-bsquare commented 1 year ago

I implemented null as a type extension. Currently nil (lua) === undefined (js) and that mapping makes sense between those two languages. The only problem with null as a type extension is that it evaluates to truthy rather than falsy sadly. Example code below:

class NullTypeExtension extends LuaTypeExtension<null> {
  private gcPointer: number;

  public constructor(thread: LuaGlobal) {
    super(thread, 'js_null');

    this.gcPointer = thread.lua.module.addFunction((functionStateAddress: LuaState) => {
      // Throws a lua error which does a jump if it does not match.
      const userDataPointer = thread.lua.luaL_checkudata(functionStateAddress, 1, this.name);
      const referencePointer = thread.lua.module.getValue(userDataPointer, '*');
      thread.lua.unref(referencePointer);

      return LuaReturn.Ok;
    }, 'ii');

    if (thread.lua.luaL_newmetatable(thread.address, this.name)) {
      const metatableIndex = thread.lua.lua_gettop(thread.address);

      // Mark it as uneditable
      thread.lua.lua_pushstring(thread.address, 'protected metatable');
      thread.lua.lua_setfield(thread.address, metatableIndex, '__metatable');

      // Add the gc function
      thread.lua.lua_pushcclosure(thread.address, this.gcPointer, 0);
      thread.lua.lua_setfield(thread.address, metatableIndex, '__gc');

      // Add an __index method that returns nothing.
      thread.pushValue(() => null);
      thread.lua.lua_setfield(thread.address, metatableIndex, '__index');

      thread.pushValue(() => 'null');
      thread.lua.lua_setfield(thread.address, metatableIndex, '__tostring');

      thread.pushValue((self: unknown, other: unknown) => self === other);
      thread.lua.lua_setfield(thread.address, metatableIndex, '__eq');
    }
    // Pop the metatable from the stack.
    thread.lua.lua_pop(thread.address, 1);

    // Create a new table, this is unique and will be the "null" value by attaching the
    // metatable created above. The first argument is the target, the second options.
    super.pushValue(thread, decorate({}, {}));
    // Put it into the global field named null.
    thread.lua.lua_setglobal(thread.address, 'null');
  }

  public getValue(thread: LuaThread, index: number): null {
    const refUserData = thread.lua.luaL_testudata(thread.address, index, this.name);
    if (!refUserData) {
      throw new Error(`data does not have the expected metatable: ${this.name}`);
    }
    return null;
  }

  // any because LuaDecoration is not exported from the Lua lib.
  public pushValue(thread: LuaThread, decoration: any): boolean {
    if (decoration?.target !== null) {
      return false;
    }
    // Rather than pushing a new value, get the global "null" onto the stack.
    thread.lua.lua_getglobal(thread.address, 'null');
    return true;
  }

  public close(): void {
    this.thread.lua.module.removeFunction(this.gcPointer);
  }
}

export default function createTypeExtension(thread: LuaGlobal): LuaTypeExtension<null> {
  return new NullTypeExtension(thread);
}
tims-bsquare commented 8 months ago

Implemented in PR https://github.com/ceifa/wasmoon/pull/101

tims-bsquare commented 8 months ago

@ceifa can close this now :)