wasmoon

repository·main·Indexed 20 days ago

https://github.com/ceifa/wasmoon

A Lua VM with JavaScript bindings implemented via WebAssembly, designed to embed a Lua engine into Node.js, Deno, or Web applications. It provides a high-performance bridge for interoperability between Lua and JavaScript, including DOM support for web environments and a CLI for executing Lua scripts or running an interactive REPL. Version 1.16.0.

Tokens
4.4K
Snippets
16
Records
26
Agent score
72%

What's inside wasmoon

  1. Compare Wasmoon and Fengari

    main

    Wasmoon compiles official Lua code to WebAssembly (Wasm), whereas Fengari is a Lua VM rewritten in JavaScript.

    • Performance: Wasmoon is generally much faster for executing Lua code due to Wasm. However, if your application requires heavy interop (frequent calls between JS and Lua), the performance advantage may diminish.
    • Size: Fengari is smaller and better for web environments where bundle size is critical.
    Metricwasmoonfengari
    Plain393kB214kB
    Gzipped130kB69kB
  2. Initialize and use Wasmoon in JavaScript

    main

    To embed Lua in a Node.js, Deno, or Web application, use LuaFactory to create a Lua engine. You can then interact with the Lua global scope by setting JavaScript functions as Lua globals, executing Lua code strings, and retrieving Lua globals as JavaScript functions.

    Important: Always call lua.global.close() in a finally block to prevent memory leaks and ensure the Lua environment is freed.

    const { LuaFactory } = require('wasmoon')
    
    // Initialize a new lua environment factory
    // You can pass the wasm location as the first argument for web environments
    const factory = new LuaFactory()
    // Create a standalone lua environment from the factory
    const lua = await factory.createEngine()
    
    try {
        // Set a JS function to be a global lua function
        lua.global.set('sum', (x, y) => x + y)
        // Run a lua string
        await lua.doString(`
        print(sum(10, 10))
        function multiply(x, y)
            return x * y
        end
        `)
        // Get a global lua function as a JS function
        const multiply = lua.global.get('multiply')
        console.log(multiply(10, 10))
    } finally {
        // Close the lua environment, so it can be freed
        lua.global.close()
    }
  3. How Lua tables map to JavaScript objects and arrays

    main

    Wasmoon automatically maps Lua tables to JavaScript types based on their structure. When retrieving a Lua table from the Lua environment, it is converted into either a standard JavaScript Array or a plain JavaScript Object (Record<any, any>).

    • Arrays: If the Lua table is sequential (keys are numeric and consecutive starting from 1), it is mapped to a JavaScript Array.
    • Objects: If the Lua table contains non-sequential keys or string keys, it is mapped to a JavaScript Object.

    This mapping is handled by the TableType extension, ensuring that data passed between the Lua VM and the JavaScript host maintains a predictable structure.

  4. Fix bundle/require errors in Webpack, Rollup, or Angular

    main

    When using Wasmoon in a browser environment, bundlers may attempt to resolve Node.js modules (like path or fs) that are not actually used. You must tell your bundler to ignore these modules.

    Webpack

    Add resolve.fallback to your config:

    module.exports = {
        resolve: {
            fallback: {
                path: false,
                fs: false,
                child_process: false,
                crypto: false,
                url: false,
                module: false,
            },
        },
    }

    Rollup

    Use rollup-plugin-ignore:

    import ignore from 'rollup-plugin-ignore';
    export default {
        plugins: [ignore(['path', 'fs', 'child_process', 'crypto', 'url', 'module'])],
    }

    Angular

    Add a browser section to your package.json:

    {
        "browser": {
            "child_process": false,
            "fs": false,
            "path": false,
            "crypto": false,
            "url": false,
            "module": false
        }
    }
  5. Handle Async/Await limitations in Lua callbacks

    main

    It is not possible to await a Promise inside a callback that is called from JS into Lua (e.g., yielding at the top-level of a file or within a callback). This results in errors like cannot resume dead coroutine or attempt to yield across a C-call boundary.

    Workaround: You can wrap the callback in a coroutine and use a helper function to manage the execution flow via a Promise.

    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
  6. Await Promises from Lua using :await()

    main

    You can await a JavaScript Promise from within Lua by calling the :await() method on the Promise object. This yields the Lua execution until the promise completes.

    const { LuaFactory } = require('wasmoon')
    const factory = new LuaFactory()
    const lua = await factory.createEngine()
    
    try {
        lua.global.set('sleep', (length) => new Promise((resolve) => setTimeout(resolve, length)))
        await lua.doString(`
            sleep(1000):await()
        `)
    } finally {
        lua.global.close()
    }
  7. Configure the Lua engine with CreateEngineOptions

    main

    When initializing a Lua engine, you can pass a CreateEngineOptions object to customize the environment. Key options include:

    • openStandardLibs: If true, injects standard Lua libraries like math, coroutine, and debug.
    • injectObjects: If true, injects specific JS objects into the Lua environment: Error, Promise, null, and Objects.
    • enableProxy: Enables a proxy for JS objects, which is useful when working with classes.
    • traceAllocations: Enables tracing of memory allocations.
    • functionTimeout: Sets the maximum time in milliseconds a Lua function can run before being interrupted.
    const options: CreateEngineOptions = {
      openStandardLibs: true,
      injectObjects: true,
      enableProxy: true,
      functionTimeout: 1000
    };
  8. Use the Wasmoon CLI

    main

    Wasmoon can be run from the command line. Use the following syntax:

    wasmoon [options] [file] [args]

    Options:

    • -l: Include a file or directory.
    • -i: Enter interactive mode after running the files.

    Example: wasmoon -i sum.lua 10 30

    Shebang Support: You can use wasmoon as a script interpreter on Unix-like systems by adding a shebang to your Lua file:

    #!/usr/bin/env wasmoon
    return arg[1] + arg[2]
    $: wasmoon [options] [file] [args]
    
    # Options:
    # -l: Include a file or directory
    # -i: Enter interactive mode after running the files
    
    $: wasmoon -i sum.lua 10 30
  9. Understand LuaReturn and LuaResumeResult

    main

    When resuming a Lua thread or executing code, the result is captured in a LuaResumeResult object. This object contains a result of type LuaReturn, which indicates the status of the execution:

    • LuaReturn.Ok (0): Execution completed successfully.
    • LuaReturn.Yield (1): The Lua code yielded.
    • LuaReturn.ErrorRun (2): An error occurred during execution.
    • LuaReturn.ErrorSyntax (3): A syntax error was encountered.
    • LuaReturn.ErrorMem (4): An error occurred due to memory issues.
    • LuaReturn.ErrorErr (5): A general error.
    • LuaReturn.ErrorFile (6): An error occurred related to a file.

    The LuaResumeResult also includes resultCount, representing the number of values returned.

    // Example of the shape of a LuaResumeResult
    const result: LuaResumeResult = {
      result: LuaReturn.Ok,
      resultCount: 1
    };
  10. Decorate JavaScript objects for Lua interop

    main

    Use the decorate function to wrap a JavaScript object with a Decoration instance. This is used to prepare objects for Lua interop, allowing you to provide configuration such as a custom metatable via BaseDecorationOptions.

    import { decorate } from 'wasmoon';
    
    const myTarget = { key: 'value' };
    const decoration = decorate(myTarget, { metatable: myCustomMetatable });
    // decoration is an instance of Decoration containing myTarget and the options
  11. Reference LuaType values

    main

    The LuaType enum represents the different types of values available in the Lua environment. Use these when checking the type of a value retrieved from the Lua state.

    enum LuaType {
        None = -1,
        Nil = 0,
        Boolean = 1,
        LightUserdata = 2,
        Number = 3,
        String = 4,
        Table = 5,
        Function = 6,
        Userdata = 7,
        Thread = 8,
    }
  12. Wasmoon CLI options and flags

    main

    The Wasmoon CLI supports the following options:

    FlagDescription
    -l <path>Include path. Mounts a file or a directory (recursively) into the Lua environment.
    -iInteractive mode. Forces the CLI into interactive REPL mode.

    Positional Arguments:

    • The first non-option argument is treated as the run file if the input is a TTY.
    • Subsequent arguments are passed to the Lua environment as the arg table.
    # Reference of CLI flags
    # -l <path> : include files/dirs
    # -i       : force interactive
    # <file>   : the lua file to run
    # <args>   : arguments passed to Lua 'arg' table