wasmoon 简单机制说明
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