wasmoon 简单机制说明

发布时间 2023-12-01 10:12:43作者: 荣锋亮

wasmoon 是基于webassembly 的lua 虚拟机,实现上直接服用了lua 的代码,通过emscripten 编译为webassembly
然后基于typescript 包装了一些操作,方便业务使用,以下是一个简单的说明

构建

wasmoon 项目使用了多模块,lua 是通过子模块引入的,同时会基于emscripten 先将lua 代码编译为一个webassembly 模块
当然其中export 了一些方法,方便typescript 包装的时候使用(也属于lua 虚拟机核心部分)因为需要使用os 的一些操作,wasmoon
也导出了emscripten 关于fs 的处理(wasi 规范的)

  • 构建命令
    build.sh
 
#!/bin/bash -e
cd $(dirname $0)
mkdir -p build
 
LUA_SRC=$(ls ./lua/*.c | grep -v "luac.c" | grep -v "lua.c" | tr "\n" " ")
 
extension=""
if [ "$1" == "dev" ];
then
    extension="$extension -O0 -g3 -s ASSERTIONS=1 -s SAFE_HEAP=1 -s STACK_OVERFLOW_CHECK=2"
else
    extension="$extension -O3 --closure 1"
fi
 
if [[ "$OSTYPE" == "darwin"* ]]; then
    sed -i '' "s/^#define LUA_32BITS\t0$/#define LUA_32BITS\t1/" ./lua/luaconf.h
else
    sed -i "s/^#define LUA_32BITS\t0$/#define LUA_32BITS\t1/" ./lua/luaconf.h
fi
# 此处暴露一些运行时代码FS,
emcc \
    -s WASM=1 $extension -o ./build/glue.js \
    -s EXPORTED_RUNTIME_METHODS="[
        'ccall', \
        'addFunction', \
        'removeFunction', \
        'FS', \
        'ENV', \
        'getValue', \
        'setValue', \
        'lengthBytesUTF8', \
        'stringToUTF8', \
        'stringToNewUTF8'
    ]" \
    -s INCOMING_MODULE_JS_API="[
        'locateFile', \
        'preRun'
    ]" \
    -s STRICT_JS=0 \
    -s MODULARIZE=1 \
    -s ALLOW_TABLE_GROWTH=1 \
    -s EXPORT_NAME="initWasmModule" \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s STRICT=1 \
    -s EXPORT_ES6=1 \
    -s NODEJS_CATCH_EXIT=0 \
    -s NODEJS_CATCH_REJECTION=0 \
    -s MALLOC=emmalloc \
    -s STACK_SIZE=1MB \
     # 主要暴露一些lua 的核心方法
    -s EXPORTED_FUNCTIONS="[
        '_malloc', \
        '_free', \
        '_realloc', \
        '_luaL_checkversion_', \
        '_luaL_getmetafield', \
        '_luaL_callmeta', \
        '_luaL_tolstring', \
        '_luaL_argerror', \
        '_luaL_typeerror', \
        '_luaL_checklstring', \
        '_luaL_optlstring', \
        '_luaL_checknumber', \
        '_luaL_optnumber', \
        '_luaL_checkinteger', \
        '_luaL_optinteger', \
        '_luaL_checkstack', \
        '_luaL_checktype', \
        '_luaL_checkany', \
        '_luaL_newmetatable', \
        '_luaL_setmetatable', \
        '_luaL_testudata', \
        '_luaL_checkudata', \
        '_luaL_where', \
        '_luaL_fileresult', \
        '_luaL_execresult', \
        '_luaL_ref', \
        '_luaL_unref', \
        '_luaL_loadfilex', \
        '_luaL_loadbufferx', \
        '_luaL_loadstring', \
        '_luaL_newstate', \
        '_luaL_len', \
        '_luaL_addgsub', \
        '_luaL_gsub', \
        '_luaL_setfuncs', \
        '_luaL_getsubtable', \
        '_luaL_traceback', \
        '_luaL_requiref', \
        '_luaL_buffinit', \
        '_luaL_prepbuffsize', \
        '_luaL_addlstring', \
        '_luaL_addstring', \
        '_luaL_addvalue', \
        '_luaL_pushresult', \
        '_luaL_pushresultsize', \
        '_luaL_buffinitsize', \
        '_lua_newstate', \
        '_lua_close', \
        '_lua_newthread', \
        '_lua_resetthread', \
        '_lua_atpanic', \
        '_lua_version', \
        '_lua_absindex', \
        '_lua_gettop', \
        '_lua_settop', \
        '_lua_pushvalue', \
        '_lua_rotate', \
        '_lua_copy', \
        '_lua_checkstack', \
        '_lua_xmove', \
        '_lua_isnumber', \
        '_lua_isstring', \
        '_lua_iscfunction', \
        '_lua_isinteger', \
        '_lua_isuserdata', \
        '_lua_type', \
        '_lua_typename', \
        '_lua_tonumberx', \
        '_lua_tointegerx', \
        '_lua_toboolean', \
        '_lua_tolstring', \
        '_lua_rawlen', \
        '_lua_tocfunction', \
        '_lua_touserdata', \
        '_lua_tothread', \
        '_lua_topointer', \
        '_lua_arith', \
        '_lua_rawequal', \
        '_lua_compare', \
        '_lua_pushnil', \
        '_lua_pushnumber', \
        '_lua_pushinteger', \
        '_lua_pushlstring', \
        '_lua_pushstring', \
        '_lua_pushcclosure', \
        '_lua_pushboolean', \
        '_lua_pushlightuserdata', \
        '_lua_pushthread', \
        '_lua_getglobal', \
        '_lua_gettable', \
        '_lua_getfield', \
        '_lua_geti', \
        '_lua_rawget', \
        '_lua_rawgeti', \
        '_lua_rawgetp', \
        '_lua_createtable', \
        '_lua_newuserdatauv', \
        '_lua_getmetatable', \
        '_lua_getiuservalue', \
        '_lua_setglobal', \
        '_lua_settable', \
        '_lua_setfield', \
        '_lua_seti', \
        '_lua_rawset', \
        '_lua_rawseti', \
        '_lua_rawsetp', \
        '_lua_setmetatable', \
        '_lua_setiuservalue', \
        '_lua_callk', \
        '_lua_pcallk', \
        '_lua_load', \
        '_lua_dump', \
        '_lua_yieldk', \
        '_lua_resume', \
        '_lua_status', \
        '_lua_isyieldable', \
        '_lua_setwarnf', \
        '_lua_warning', \
        '_lua_error', \
        '_lua_next', \
        '_lua_concat', \
        '_lua_len', \
        '_lua_stringtonumber', \
        '_lua_getallocf', \
        '_lua_setallocf', \
        '_lua_toclose', \
        '_lua_closeslot', \
        '_lua_getstack', \
        '_lua_getinfo', \
        '_lua_getlocal', \
        '_lua_setlocal', \
        '_lua_getupvalue', \
        '_lua_setupvalue', \
        '_lua_upvalueid', \
        '_lua_upvaluejoin', \
        '_lua_sethook', \
        '_lua_gethook', \
        '_lua_gethookmask', \
        '_lua_gethookcount', \
        '_lua_setcstacklimit', \
        '_luaopen_base', \
        '_luaopen_coroutine', \
        '_luaopen_table', \
        '_luaopen_io', \
        '_luaopen_os', \
        '_luaopen_string', \
        '_luaopen_utf8', \
        '_luaopen_math', \
        '_luaopen_debug', \
        '_luaopen_package', \
        '_luaL_openlibs' \
    ]" \
    ${LUA_SRC}
 
if [[ "$OSTYPE" == "darwin"* ]]; then
    sed -i '' "s/^#define LUA_32BITS\t1$/#define LUA_32BITS\t0/" ./lua/luaconf.h
else
    sed -i "s/^#define LUA_32BITS\t1$/#define LUA_32BITS\t0/" ./lua/luaconf.h
fi

包装

有一个方便的emscripten typescript 类型定义包@types/emscripten wasmoon 集成了此包,方便使用

  • 对于生成的wasm包装
    核心是src/luawasm.ts
    参考处理LuaWasm 类定义
 
export default class LuaWasm {
 
    public static async initialize(customWasmFileLocation?: string, environmentVariables?: EnvironmentVariables): Promise<LuaWasm> {
        // 初始化wasm 模块
        const module: LuaEmscriptenModule = await initWasmModule({
            locateFile: (path: string, scriptDirectory: string) => {
                return customWasmFileLocation || scriptDirectory + path
            },
            preRun: (initializedModule: LuaEmscriptenModule) => {
                if (typeof environmentVariables === 'object') {
                    Object.entries(environmentVariables).forEach(([k, v]) => (initializedModule.ENV[k] = v))
                }
            },
        })
        return new LuaWasm(module)
    }

构造函数

public constructor(module: LuaEmscriptenModule) {
    this.module = module
    //  通过emscripten 的cwrap 调用暴露的lua访问
    this.luaL_checkversion_ = this.cwrap('luaL_checkversion_', null, ['number', 'number', 'number'])
    this.luaL_getmetafield = this.cwrap('luaL_getmetafield', 'number', ['number', 'number', 'string'])
    this.luaL_callmeta = this.cwrap('luaL_callmeta', 'number', ['number', 'number', 'string'])
    this.luaL_tolstring = this.cwrap('luaL_tolstring', 'string', ['number', 'number', 'number'])
    this.luaL_argerror = this.cwrap('luaL_argerror', 'number', ['number', 'number', 'string'])
    this.luaL_typeerror = this.cwrap('luaL_typeerror', 'number', ['number', 'number', 'string'])
    this.luaL_checklstring = this.cwrap('luaL_checklstring', 'string', ['number', 'number', 'number'])
    this.luaL_optlstring = this.cwrap('luaL_optlstring', 'string', ['number', 'number', 'string', 'number'])
    this.luaL_checknumber = this.cwrap('luaL_checknumber', 'number', ['number', 'number'])
    this.luaL_optnumber = this.cwrap('luaL_optnumber', 'number', ['number', 'number', 'number'])
    this.luaL_checkinteger = this.cwrap('luaL_checkinteger', 'number', ['number', 'number'])
  • 使用LuaWasm
    核心是通过factory 创建的,内部实际执行是通过engine
    factory
 
export default class LuaFactory {
    private luaWasmPromise: Promise<LuaWasm>
    // 构造函数初始化
    public constructor(customWasmUri?: string, environmentVariables?: EnvironmentVariables) {
        if (customWasmUri === undefined) {
            const isBrowser =
                (typeof window === 'object' && typeof window.document !== 'undefined') ||
                (typeof self === 'object' && self?.constructor?.name === 'DedicatedWorkerGlobalScope')
 
            if (isBrowser) {
                const majorminor = version.slice(0, version.lastIndexOf('.'))
                customWasmUri = `https://unpkg.com/wasmoon@${majorminor}/dist/glue.wasm`
            }
        }
 
        this.luaWasmPromise = LuaWasm.initialize(customWasmUri, environmentVariables)
    }
   // 基于虚拟文件的挂载
    public async mountFile(path: string, content: string | ArrayBufferView): Promise<void> {
        this.mountFileSync(await this.getLuaModule(), path, content)
    }
    // 基于虚拟文件的挂载,此处包装感觉并不是很方便,对于node 使用不是特别方便
    public mountFileSync(luaWasm: LuaWasm, path: string, content: string | ArrayBufferView): void {
        const fileSep = path.lastIndexOf('/')
        const file = path.substring(fileSep + 1)
        const body = path.substring(0, path.length - file.length - 1)
 
        if (body.length > 0) {
            const parts = body.split('/').reverse()
            let parent = ''
 
            while (parts.length) {
                const part = parts.pop()
                if (!part) {
                    continue
                }
 
                const current = `${parent}/${part}`
                try {
                    luaWasm.module.FS.mkdir(current)
                } catch (err) {
                    // ignore EEXIST
                }
 
                parent = current
            }
        }
        luaWasm.module.FS.writeFile(path, content)
    }
    // 创建引擎
    public async createEngine(options: ConstructorParameters<typeof LuaEngine>[1] = {}): Promise<LuaEngine> {
        return new LuaEngine(await this.getLuaModule(), options)
    }
 
    public async getLuaModule(): Promise<LuaWasm> {
        return this.luaWasmPromise
    }
}

engine 处理(核心的lua 执行)

export default class LuaEngine {
    public global: Global
 
    public constructor(
        private cmodule: LuaWasm,
        { openStandardLibs = true, injectObjects = false, enableProxy = true, traceAllocations = false } = {},
    ) {
        this.global = new Global(this.cmodule, traceAllocations)
 
        // Generic handlers - These may be required to be registered for additional types.
        this.global.registerTypeExtension(0, createTableType(this.global))
        this.global.registerTypeExtension(0, createFunctionType(this.global))
 
        // Contains the :await functionality.
        this.global.registerTypeExtension(1, createPromiseType(this.global, injectObjects))
 
        if (enableProxy) {
            // This extension only really overrides tables and arrays.
            // When a function is looked up in one of it's tables it's bound and then
            // handled by the function type extension.
            this.global.registerTypeExtension(3, createProxyType(this.global))
        } else {
            // No need to register this when the proxy is enabled.
            this.global.registerTypeExtension(1, createErrorType(this.global, injectObjects))
        }
 
        // Higher priority than proxied objects to allow custom user data without exposing methods.
        this.global.registerTypeExtension(4, createUserdataType(this.global))
 
        if (openStandardLibs) {
            this.cmodule.luaL_openlibs(this.global.address)
        }
    }

说明

wasmoon 核心还是基于emscripten 生成lua 的webassembly 文件,之后基于typescript 进行包装

参考资料

https://github.com/ceifa/wasmoon
https://emscripten.org/
https://github.com/lua/lua