WAVM源码解析 —— WASI接口定义、内部实例初始化及实例链
前言
从前面两篇文章中,我们可以窥探,WAVM执行一个wasm程序,主要包括一下步骤:
-
加载wasm二进制文件到内存,解析生成 IR::Module
在这一步主要是解析wasm的各个Segment,保存到我们自定义的数据结构IR::Module中 -
编译生成本地可执行的二进制代码
这一步是实现JIT的关键,是区别于wasm解释器的地方,这样我们就可以通过wasm程序中各个函数的地址,来调用执行,同我们执行普通的C函数一样,这有点类似于加载动态链接库中的函数(见 dlsym(3)) -
生成内部实例
这一步是实现外部函数接口的关键,比如wasi定义的接口,这些接口都将在内部实例中实现 -
链接
链接的主要工作就是根据需要导入的Module和导入项的名字,在对应的实例中查找,以获取对应的内容,如导入的函数,需要获取其入口地址。
wasi定义的接口,就需要在链接的过程中,从内部实例中获取对应接口函数的地址 -
实例化
生成实例,其实生成内部实例的过程也是实例化,只不过内部实例不基于任何IR::Module,也无需链接任何内容
所谓实例化,主要内容就是为内存段、表段等申请空间,记录所有函数(自定义的函数和导入的函数)的入口地址,然后将所有的信息记录到一个统一的数据结构中 -
执行
根据提供的入口函数,比如"_start",通过实例化后的实例中的函数信息列表,找到其入口地址,然后调用
本文将重点关注上述3、4、5三个部分
生成内部实例
一、调用接口
Instance* Intrinsics::instantiateModule(
Compartment* compartment,
const std::initializer_list<const Intrinsics::Module*>& moduleRefs,
std::string&& debugName)
Instance* wasi_snapshot_preview1
= Intrinsics::instantiateModule(compartment,
{WAVM_INTRINSIC_MODULE_REF(wasi),
WAVM_INTRINSIC_MODULE_REF(wasiArgsEnvs),
WAVM_INTRINSIC_MODULE_REF(wasiClocks),
WAVM_INTRINSIC_MODULE_REF(wasiFile)},
"wasi_snapshot_preview1");
函数,最重要的参数是moduleRefs,这是一个Intrinsics::Module类型的列表,因为内部实例不是基于wasm程序的,其只需要考虑导入导出段所关注的内容,因此定义了Intrinsics::Module类,其类似与IR::Module,但是仅仅包含Function、Global、Table、Memory等内容,其结构如下:
struct ModuleImpl
{
HashMap<std::string, Intrinsics::Function*> functionMap;
HashMap<std::string, Intrinsics::Table*> tableMap;
HashMap<std::string, Intrinsics::Memory*> memoryMap;
HashMap<std::string, Intrinsics::Global*> globalMap;
};
struct Module
{
ModuleImpl* impl = nullptr;
WAVM_API ~Module();
};
WAVM_INTRINSIC_MODULE_REF(wasi)
是一个宏,将其替换可得getIntrinsicModule_wasi()
,查看其定义:
WAVM::Intrinsics::Module* getIntrinsicModule_wasi()
{
static WAVM::Intrinsics::Module module;
return &module;
}
也就是说,每一个WAVM_INTRINSIC_MODULE_REF()
都会返回一个Intrinsics::Module对象,在WAVM中,定义了4个Intrinsics::Module对象,他们是按实现的功能类别分类的,但是实际上实现的都是wasi标准定义的接口。
那么如何初始化Intrinsics::Module对象呢?
二、Intrinsics::Module的构建
构建Intrinsics::Module,需要依赖于一个宏函数WAVM_DEFINE_INTRINSIC_FUNCTION
,定义如下:
#define WAVM_DEFINE_INTRINSIC_FUNCTION(module, nameString, Result, cName, ...) \
static Result cName(WAVM::Runtime::ContextRuntimeData* contextRuntimeData, ##__VA_ARGS__); \
static WAVM::Intrinsics::Function cName##Intrinsic( \
getIntrinsicModule_##module(), \
nameString, \
(void*)&cName, \
WAVM::Intrinsics::inferIntrinsicFunctionType(&cName)); \
static Result cName(WAVM::Runtime::ContextRuntimeData* contextRuntimeData, ##__VA_ARGS__)
WAVM_DEFINE_INTRINSIC_FUNCTION
宏,定义了一个接口,同时将接口赋值给具体的Intrinsics::Module对象,我们以一个简单的sched_yield
为例:
宏定义
WAVM_DEFINE_INTRINSIC_FUNCTION(wasi,
"sched_yield",
__wasi_errno_return_t,
wasi_sched_yield)
{
TRACE_SYSCALL("sched_yield", "()");
Platform::yieldToAnotherThread();
return TRACE_SYSCALL_RETURN(__WASI_ESUCCESS);
}
转化后
static __wasi_errno_return_t wasi_sched_yield(
WAVM::Runtime::ContextRuntimeData* contextRuntimeData);
static WAVM::Intrinsics::Function wasi_sched_yieldIntrinsic(
getIntrinsicModule_wasi(),
"sched_yield",
(void*)&wasi_sched_yield,
WAVM::Intrinsics::inferIntrinsicFunctionType(&wasi_sched_yield));
static __wasi_errno_return_t wasi_sched_yield(WAVM::Runtime::ContextRuntimeData* contextRuntimeData)
{
TRACE_SYSCALL("sched_yield", "()");
Platform::yieldToAnotherThread();
return TRACE_SYSCALL_RETURN(__WASI_ESUCCESS);
}
在这里,定一个三方面内容,分别是:
- 接口的函数声明
- Intrinsics::Function类型的对象
- 接口的定义
而这里最关键的就是第二部分,构建静态的Intrinsics::Function对象,我们看一下其构造函数:
Intrinsics::Function::Function(Intrinsics::Module* moduleRef,
const char* inName,
void* inNativeFunction,
FunctionType inType)
: name(inName), type(inType), nativeFunction(inNativeFunction)
{
initializeModule(moduleRef);
if(moduleRef->impl->functionMap.contains(name))
{ Errors::fatalf("Intrinsic function already registered: %s", name); }
moduleRef->impl->functionMap.set(name, this);
}
可以看到,构造函数的一个重要作用就是,将自己赋值给Intrinsics::Module对象,换言之,每当使用WAVM_DEFINE_INTRINSIC_FUNCTION()
宏定义一个接口函数,静态的Intrinsics::Function对象会自动调用构造函数,从而将自己赋值到Intrinsics::Module中。
三、Intrinsics::instantiateModule()执行
Instance* Intrinsics::instantiateModule(
Compartment* compartment,
const std::initializer_list<const Intrinsics::Module*>& moduleRefs,
std::string&& debugName)
{。。。}
此函数,主要的步骤是:
1. 将moduleRefs转化为IR::Module
2. 编译上一步生成的IR::Module
3. 调用实例化接口函数,生成内部实例
整个过程是非常清晰的。
3.1 将moduleRefs转化为IR::Module
moduleRefs引用的是一个Intrinsics::Module列表,而Intrinsics::Module仅仅关注Function、Table、Memory、Globa四项内容,我们仅在这里分析最重要的Function部分。
首先我们看看Intrinsics::Function的数据结构:
struct Function
{
WAVM_API Function(Intrinsics::Module* moduleRef,
const char* inName,
void* inNativeFunction,
IR::FunctionType type);
private:
const char* name;
IR::FunctionType type;
void* nativeFunction;
};
主要包括,函数名、函数的签名、以及其函数指针,始终注意,所谓接口函数就是本地的CPP的函数,就是我们使用WAVM_DEFINE_INTRINSIC_FUNCTION()
宏定义的,与之相互对的是wasm的内部函数,由WASM的类型段、函数段、代码段等组成,我们看一下WAVM给wasm的函数结构定义:
struct Module
{
...
IndexSpace<FunctionDef, IndexedFunctionType> functions;
...
}
template<typename Definition, typename Type> struct IndexSpace
{
std::vector<Import<Type>> imports;
std::vector<Definition> defs;
...
}
template<typename Type> struct Import
{
Type type;
std::string moduleName;
std::string exportName;
};
struct FunctionDef
{
// 函数类型的索引,继承自原WASM的函数段
IndexedFunctionType type;
// 函数局部变量的信息,也就是每个局部变量的值类型,继承自原WASM的代码段
std::vector<ValueType> nonParameterLocalTypes;
// 函数的字节码,继承自原WASM的代码段
std::vector<U8> code;
std::vector<std::vector<Uptr>> branchTables;
};
在IR::Module中,定义了一个functions
字段,来存储函数的信息,其类型为IndexSpace
,我们查看IndexSpace
的定义,发现其包含了两部分组成:
- 导入的,即
std::vector<Import<Type>> imports;
- 自定义的,即
std::vector<Definition> defs;
对于导入的部分,其信息主要包括了类型、导入包的名字和导入导入项的名字,对于Functiong而言,类型是IndexedFunctionType
,这其实是wasm类型段的索引。
对于自定义的部分,其定义就是FunctionDef
内容包括了函数类型、局部变量表以及具体的代码段的内容。
OK,来看一下具体的转化代码,依然只关注函数的内容:
for(const Intrinsics::Module* moduleRef : moduleRefs)
{
if(moduleRef->impl)
{
for(const auto& pair : moduleRef->impl->functionMap)
{
functionImportBindings.emplace_back(pair.value->getNativeFunction());
const Uptr typeIndex = irModule.types.size();
const Uptr functionIndex = irModule.functions.size();
irModule.types.push_back(pair.value->getType());
irModule.functions.imports.push_back({{typeIndex}, "", pair.value->getName()});
irModule.imports.push_back({ExternKind::function, functionIndex});
}
过程是:
- 将接口函数的地址,写入到functionImportBindings中
- 将接口函数的类型,"依次"添加到irModule的
type
字段中, - 将从第二步中获取的
{类型索引 、空包名“ ” 、函数名}
,写入irModule的functions
字段的import
项 - 将函数索引,写入irModule的
imports
字段
- 这里需要注意,导入包名、导入项名其实应该是irModule的
imports
字段的信息,在实现山将这部分信息给到了template<typename Type> struct Import
结构; - 其实根据wasm的定义,irModule的
type
字段中是不应该包含重复项的,但是显然这个过程是不能保证的,但是这个不重要; - 我们将外部接口函数放到了
imports
字段,但是显然链接过程是需要链接export
字段的,这是因为能导出的函数,一定是自定义,而不能是导入的,这是合理的,我总不能把我导入的包再导出吧,因此外部接口是以导入项的形式存在的,显然这个导入项是不能从其他包导入的,所以导入包的名称是空,我们也不需要导入,因为外部接口函数的地址我们是知道的,就保存在functionImportBindings中; - 其实链接的过程,就是获取导入函数的地址的过程
因此接下来我们要为每一个外部接口函数生成一个wasm格式的thunks函数,然后将thunks导出。
3.2 创建thunks函数
主要过程如下;
for(Uptr functionImportIndex = 0; functionImportIndex < irModule.functions.imports.size();
++functionImportIndex)
{
const FunctionImport& functionImport = irModule.functions.imports[functionImportIndex];
const FunctionType intrinsicFunctionType = irModule.types[functionImport.type.index];
const FunctionType wasmFunctionType(
intrinsicFunctionType.results(), intrinsicFunctionType.params(), CallingConvention::wasm);
const Uptr wasmFunctionTypeIndex = irModule.types.size();
irModule.types.push_back(wasmFunctionType);
// 下面操作是将加载参数和调用函数,封装到codeStream,可以认为是WASM的调用函数命令
// WASM的函数调用,在调用函数的时候会先把参数放到操作数栈
// call指令会先将参数加载到自己的局部变量表中,然后调用local_get将其放到操作数栈进行操作
// 在这里,我们先执行了local_get,然后再进行call,然后将其放入codeStream中
Serialization::ArrayOutputStream codeStream;
OperatorEncoderStream opEncoder(codeStream);
for(Uptr paramIndex = 0; paramIndex < intrinsicFunctionType.params().size(); ++paramIndex)
{
opEncoder.local_get({paramIndex});
}
opEncoder.call({functionImportIndex});
opEncoder.end();
// 将自定义的函数再写入functions.def和export中,其实函数的字节码为codeStream
// 从这里可以看到,我们在WASM中用内部函数封装了一层外部的本地函数,内部函数所用就是执行call指令
// 而前面的local_get就是为了将参数放到操作数栈
const Uptr wasmFunctionIndex = irModule.functions.size();
irModule.functions.defs.push_back({{wasmFunctionTypeIndex}, {}, codeStream.getBytes(), {}});
irModule.exports.push_back(
{functionImport.exportName, ExternKind::function, wasmFunctionIndex});
}
- 获取thunks函数的函数类型,填入到
irModule.types
- 创建thunks函数,主要操作见注释
- 将thunks函数写入
irModule.functions.defs
,写入的内容包括了thunks函数的字节码,其与wasm的代码段的字节码是一样的 - 填充导出段
最后将执行实例化操作,创建出内部实例,内部实例和普通实例其实是相同的,区别在于导入段所需要内容的获取方式不同。
以函数为例,实例化的过程就是:
- 1. 将导入函数的地址写到指定的位置,这样在执行是就能找到对应的函数
- 2. 同时提供一个接口,可以让链接器找到本实例导出函数的地址。
以Memory为例,实例化的过程就是:
- 1. 分配内存空间、初始化内存的数据
- 2. 同上1
- 3. 同上2
在执行实例化函数之前,我们必须将导入项的内容准备好,对于函数而言,需要的就是导入函数的地址,对于内部实例,函数地址我们是直接写在
functionImportBindings中的,而对于普通实例,其导入函数的地址,需要通过链接器进行链接操作获取!那么链接器是如何工作的呢?
链接
说实话链接应该是最简单的了:
- 循环遍历自己的导入项
- 通过
moduleNameToInstanceMap.get(moduleName);
获取对应的项 - 如果没有找到,则记录未找到的导入包名、导入项以及导入类型等信息
- 如果不存在未找到的导入项,那么宣告链接成功
前面我们说过,实例化的一大作用就是为链接提供查询导出项的接口!
核心实现,实在很简单,就懒得解析了。
关于实例化的内容,见下一篇!
struct LinkResult
{
struct MissingImport
{
std::string moduleName;
std::string exportName;
IR::ExternType type;
};
// 真正起作用的是resolvedImports,如果success的话,missingImports应该是空的,他描述的是没找到的导入项
std::vector<MissingImport> missingImports;
ImportBindings resolvedImports;
bool success{false};
};
LinkResult Runtime::linkModule(const IR::Module& module, Resolver& resolver)
{
LinkResult linkResult;
for(const auto& kindIndex : module.imports)
{
switch(kindIndex.kind)
{
case ExternKind::function: {
const auto& functionImport = module.functions.imports[kindIndex.index];
linkImport(module,
functionImport,
module.types[functionImport.type.index],
resolver,
linkResult);
break;
case ExternKind::table:
case ExternKind::memory:
case ExternKind::global:
default: WAVM_UNREACHABLE();
}
};
}
linkResult.success = linkResult.missingImports.size() == 0;
return linkResult;
}
static void linkImport(const IR::Module& module,
const Import<Type>& import,
ResolvedType resolvedType,
Resolver& resolver,
LinkResult& linkResult)
{
Object* importValue;
if(resolver.resolve(import.moduleName, import.exportName, resolvedType, importValue))
{
linkResult.resolvedImports.push_back(importValue);
}
else
{
linkResult.missingImports.push_back({import.moduleName, import.exportName, resolvedType});
linkResult.resolvedImports.push_back(nullptr);
}
}
bool ProcessResolver::resolve(const std::string& moduleName,
const std::string& exportName,
ExternType type,
Object*& outObject)
{
const auto& namedInstance = moduleNameToInstanceMap.get(moduleName);
return namedInstance != nullptr;
}