WebAssembly学习(三):AssemblyScript - TypeScript到WebAssembly的编译

虽然说只要高级语言能转换成 LLVM IR,就能被编译成 WebAssembly 字节码,官方也推荐c/c++的方式,但是让一个前端工程师去熟练使用c/c++显然是有点困难,那么TypeScript 的方式便是前端编写 WebAssembly 最佳选择。

要将TypeScript 编译为WebAssembly,就要用到AssemblyScript编译器了。

AssemblyScript使用Binaryen(Emscripten的WebAssembly后端)将严格类型化的TypeScript(基本的带有类型的JavaScript)编译为WebAssembly,虽然它提供了几种新的特定于WebAssembly的类型和内置函数,但它本身并不是一种真正的语言,而是一种编译器变体。 它生成精简的WebAssembly模块,只需要一个npm安装。

1.安装

推荐使用cnpm安装,npm太慢了。

1 cnpm install --save-dev AssemblyScript/assemblyscript

执行过程如下,安装完成之后只有一个node_modules文件夹

1 E:\Code\assembly>cnpm install --save-dev AssemblyScript/assemblyscript
2 - [@AssemblyScript/assemblyscript] install  from git github:AssemblyScript/assemblyscript, may be very slow, please keep patience
3 √ Installed 1 packages
4 √ Linked 15 latest versions
5 √ Run 0 scripts
6 Recently updated (since 2019-02-17): 1 packages (detail see file E:\Code\assembly\node_modules\.recently_updates.txt)
7 √ All packages installed (14 packages installed from npm registry, 1 packages installed from git, used 1m(network 1m), speed 10.27kB/s, json 14(31.43kB), tarball 707.23kB)

2.创建项目

正确安装AssemblyScript后,它会提供一个名为asinit的小型实用工具来构建一个新项目,例如,在当前目录中:

1 npx asinit .

 就像使用express初始化项目一样,执行过程如下

 1 E:\Code\assembly>npx asinit .
 2 Version: 0.6.0
 3 
 4 This command will make sure that the following files exist in the project
 5 directory 'E:\Code\assembly':
 6 
 7   ./assembly
 8   Directory holding the AssemblyScript sources being compiled to WebAssembly.
 9 
10   ./assembly/tsconfig.json
11   TypeScript configuration inheriting recommended AssemblyScript settings.
12 
13   ./assembly/index.ts
14   Exemplary entry file being compiled to WebAssembly to get you started.
15 
16   ./build
17   Build artifact directory where compiled WebAssembly files are stored.
18 
19   ./build/.gitignore
20   Git configuration that excludes compiled binaries from source control.
21 
22   ./index.js
23   Main file loading the WebAssembly module and exporting its exports.
24 
25   ./package.json
26   Package info containing the necessary commands to compile to WebAssembly.
27 
28 The command will try to update existing files to match the correct settings
29 for this instance of the compiler in 'E:\Code\assembly\node_modules\_assemblyscript@0.6.0@assemblyscript'.
30 
31 Do you want to proceed? [Y/n] y
32 
33 - Making sure that the project directory exists...
34   Exists: E:\Code\assembly
35 
36 - Making sure that the 'assembly' directory exists...
37   Created: E:\Code\assembly\assembly
38 
39 - Making sure that 'assembly/tsconfig.json' is set up...
40   Created: E:\Code\assembly\assembly\tsconfig.json
41 
42 - Making sure that 'assembly/index.ts' exists...
43   Created: E:\Code\assembly\assembly\index.ts
44 
45 - Making sure that the 'build' directory exists...
46   Created: E:\Code\assembly\build
47 
48 - Making sure that 'build/.gitignore' is set up...
49   Created: E:\Code\assembly\build\.gitignore
50 
51 - Making sure that 'package.json' contains the build commands...
52   Updated: E:\Code\assembly\package.json
53 
54 - Making sure that 'index.js' exists...
55   Created: E:\Code\assembly\index.js
56 
57 Done!
58 
59 To edit the entry file, open 'assembly/index.ts' in your editor of choice.
60 Create as many additional files as necessary and use them as imports.
61 
62 To build the entry file to WebAssembly when you are ready, run:
63 
64   npm run asbuild
65 
66 Running the command above creates the following binaries incl. their respective
67 text format representations and source maps:
68 
69   ./build/untouched.wasm
70   ./build/untouched.wasm.map
71   ./build/untouched.wat
72 
73   ^ The untouched WebAssembly module as generated by the compiler.
74     This one matches your sources exactly, without any optimizations.
75 
76   ./build/optimized.wasm
77   ./build/optimized.wasm.map
78   ./build/optimized.wat
79 
80   ^ The optimized WebAssembly module using default optimization settings (-O2s).
81     You can change the optimization settings in 'package.json'.
82 
83 Additional documentation is available at the AssemblyScript wiki:
84 
85   https://github.com/AssemblyScript/assemblyscript/wiki
86 
87 Have a nice day!
88 
89 E:\Code\assembly>

完成的目录结构是这样的:

目录结构含义:

assembly / index.ts上的示例性条目文件

index.js中的通用索引文件,用于加载已编译的二进制文件和必要的配置文件,如package.json和tsconfig.json

初始化后,只需在编码时使用现有的TypeScript工具,并使用编译器手动构建到WebAssembly,或者使用生成的构建任务:按照提示,运行以下命令:

1 npm run asbuild

build文件夹初始化只有一个.gitignore,该命令会在build文件夹中创建.wat与.wasm文件

以上只是构建一个示例项目,如果需要适合开发的安装,可以通过克隆GitHub存储库来实现:

1 $> git clone https://github.com/AssemblyScript/assemblyscript.git
2 $> cd assemblyscript
3 $> npm install
4 $> npm link

 请注意,编译器的新克隆将使用dist /中的分发文件,但它也可以在npm运行清理后直接通过ts节点运行源,这在开发中很有用。 也可以通过运行asc -v来检查这种情况(如果它声明-dev则运行源)。

3.使用编译器

类似于TypeScript的tsc编译为JavaScript,AssemblyScript的asc编译为WebAssembly:

例如,用 TypeScript 实现斐波那契序列计算的模块 f.ts 如下:

1 export function f(x: i32): i32 {
2     if (x === 1 || x === 2) {
3         return 1;
4     }
5     return f(x - 1) + f(x - 2)
6 }

执行以下asc命令,就能把以上代码编译成可运行的 WebAssembly 模块,比起c/c++的编译方式,这种人性化多了。

1 asc f.ts -o f.wasm

以上代码中出现了一个新的内置类型 i32,这是 AssemblyScript 在 TypeScript 的基础上内置的类型。 AssemblyScript 和 TypeScript 有细微区别,AssemblyScript 是 TypeScript 的子集,为了方便编译成 WebAssembly 在 TypeScript 的基础上加了更严格的类型限制, 区别如下:

  • 比 TypeScript 多了很多更细致的内置类型,以优化性能和内存占用,详情文档;
  • 不能使用 any 和 undefined 类型,以及枚举类型;
  • 可空类型的变量必须是引用类型,而不能是基本数据类型如 string、number、boolean;
  • 函数中的可选参数必须提供默认值,函数必须有返回类型,无返回值的函数返回类型需要是 void;
  • 不能使用 JS 环境中的内置函数,只能使用 AssemblyScript 提供的内置函数

总体来说 AssemblyScript 比 TypeScript 又多了很多限制,编写起来会觉得局限性很大; 用 AssemblyScript 来写 WebAssembly 经常会出现 tsc 编译通过但运行 WebAssembly 时出错的情况,这很可能就是你没有遵守以上限制导致的;但 AssemblyScript 通过修改 TypeScript 编译器默认配置能在编译阶段找出大多错误。

AssemblyScript 的实现原理其实也借助了 LLVM,它通过 TypeScript 编译器把 TS 源码解析成 AST,再把 AST 翻译成 IR,再通过 LLVM 编译成 WebAssembly 字节码实现; 上面提到的各种限制都是为了方便把 AST 转换成 LLVM IR。

asc编译器具体API如下

 1 SYNTAX
 2   asc [entryFile ...] [options]
 3 
 4 EXAMPLES
 5   asc hello.ts
 6   asc hello.ts -b hello.wasm -t hello.wat
 7   asc hello1.ts hello2.ts -b -O > hello.wasm
 8 
 9 OPTIONS
10   --version, -v         Prints just the compiler's version and exits.
11   --help, -h            Prints this message and exits.
12   --optimize, -O        Optimizes the module. Also has the usual shorthands:
13 
14                          -O     Uses defaults. Equivalent to -O2s
15                          -O0    Equivalent to --optimizeLevel 0
16                          -O1    Equivalent to --optimizeLevel 1
17                          -O2    Equivalent to --optimizeLevel 2
18                          -O3    Equivalent to --optimizeLevel 3
19                          -Oz    Equivalent to -O but with --shrinkLevel 2
20                          -O3s   Equivalent to -O3 with --shrinkLevel 1 etc.
21 
22   --optimizeLevel       How much to focus on optimizing code. [0-3]
23   --shrinkLevel         How much to focus on shrinking code size. [0-2, s=1, z=2]
24   --validate, -c        Validates the module using Binaryen. Exits if invalid.
25   --baseDir             Specifies the base directory of input and output files.
26   --outFile, -o         Specifies the output file. File extension indicates format.
27   --binaryFile, -b      Specifies the binary output file (.wasm).
28   --textFile, -t        Specifies the text output file (.wat).
29   --asmjsFile, -a       Specifies the asm.js output file (.js).
30   --idlFile, -i         Specifies the WebIDL output file (.webidl).
31   --tsdFile, -d         Specifies the TypeScript definition output file (.d.ts).
32   --sourceMap           Enables source map generation. Optionally takes the URL
33                         used to reference the source map from the binary file.
34   --debug               Enables debug information in emitted binaries.
35   --noAssert            Replaces assertions with just their value without trapping.
36   --noEmit              Performs compilation as usual but does not emit code.
37   --importMemory        Imports the memory instance provided by the embedder.
38   --sharedMemory        Declare memory as shared by settings the max shared memory.
39   --memoryBase          Sets the start offset of compiler-generated static memory.
40   --importTable         Imports the function table instance provided by the embedder.
41   --noLib               Does not include the shipped standard library.
42   --lib                 Adds one or multiple paths to custom library components and
43                         uses exports of all top-level files at this path as globals.
44   --use, -u             Aliases a global object under another name, e.g., to switch
45                         the default 'Math' implementation used: --use Math=JSMath
46   --trapMode            Sets the trap mode to use.
47 
48                          allow  Allow trapping operations. This is the default.
49                          clamp  Replace trapping operations with clamping semantics.
50                          js     Replace trapping operations with JS semantics.
51 
52   --runPasses           Specifies additional Binaryen passes to run after other
53                         optimizations, if any. See: Binaryen/src/passes/pass.cpp
54   --enable              Enables additional (experimental) WebAssembly features.
55 
56                          sign-extension  Enables sign-extension operations
57                          mutable-global  Enables mutable global imports and exports
58                          bulk-memory     Enables bulk memory operations
59                          simd            Enables SIMD types and operations.
60                          threads         Enables threading and atomic operations.
61 
62   --transform           Specifies the path to a custom transform to 'require'.
63   --measure             Prints measuring information on I/O and compile times.
64   --noColors            Disables terminal colors.

编译器API也可以以编程方式使用。 它接受与CLI相同的选项,但也允许您覆盖stdout和stderr和/或提供回调:

 1 const asc = require("assemblyscript/cli/asc");
 2 asc.main([
 3   "myModule.ts",
 4   "--binaryFile", "myModule.wasm",
 5   "--optimize",
 6   "--sourceMap",
 7   "--measure"
 8 ], {
 9   stdout: process.stdout,
10   stderr: process.stderr
11 }, function(err) {
12   if (err)
13     throw err;
14   ...
15 });

可以通过编程方式获取可用的命令行选项:

1 const options = require("assemblyscript/cli/asc.json");
2 ...

您也可以直接编译源字符串,例如在浏览器环境中:

1 const { binary, text, stdout, stderr } = asc.compileString(`...`, { optimize: 2 });
2 ... 

4.使用编译的.wasm模块

index.js示例代码如下:

1 const fs = require("fs");
2 const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
3 const imports = {};
4 Object.defineProperty(module, "exports", {
5   get: () => new WebAssembly.Instance(compiled, imports).exports
6 });

使用上面编译的f.wasm

1 fetch('f.wasm') // 网络加载 f.wasm 文件
2      .then(res => res.arrayBuffer()) // 转成 ArrayBuffer
3      .then(WebAssembly.instantiate) // 编译为当前 CPU 架构的机器码 + 实例化
4      .then(mod => { // 调用模块实例上的 f 函数计算
5      console.log(mod.instance.f(50));
6      });

5.参考文献:

GitHub—AssemblyScript 开源项目

吴浩麟—WebAssembly 现状与实战

posted @ 2019-02-25 00:15  jixhua  阅读(5322)  评论(1编辑  收藏  举报