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三个部分
生成内部实例
一、调用接口
1 2 3 4 5 6 7 8 9 10 11 12
| 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等内容,其结构如下:
1 2 3 4 5 6 7 8 9 10 11 12
| 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(),查看其定义:
1 2 3 4 5
| 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,定义如下:
1 2 3 4 5 6 7 8
| #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为例:
宏定义
1 2 3 4 5 6 7 8 9
| 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); }
|
转化后
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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对象,我们看一下其构造函数:
1 2 3 4 5 6 7 8 9 10 11 12
| 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()执行
1 2 3 4 5
| 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的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12
| 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的函数结构定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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 { IndexedFunctionType type; std::vector<ValueType> nonParameterLocalTypes; 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,来看一下具体的转化代码,依然只关注函数的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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函数
主要过程如下;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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);
Serialization::ArrayOutputStream codeStream; OperatorEncoderStream opEncoder(codeStream); for(Uptr paramIndex = 0; paramIndex < intrinsicFunctionType.params().size(); ++paramIndex) { opEncoder.local_get({paramIndex}); } opEncoder.call({functionImportIndex}); opEncoder.end(); 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);获取对应的项
- 如果没有找到,则记录未找到的导入包名、导入项以及导入类型等信息
- 如果不存在未找到的导入项,那么宣告链接成功
前面我们说过,实例化的一大作用就是为链接提供查询导出项的接口!
核心实现,实在很简单,就懒得解析了。
关于实例化的内容,见下一篇!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| struct LinkResult { struct MissingImport { std::string moduleName; std::string exportName; IR::ExternType type; }; 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; }
|