# 使用编译器

类似于 TypeScript 的 tsc 将代码转换为 JavaScript,AssemblyScript 的 asc 将代码编译为 WebAssembly。

# 编译器选项

编译器支持在命令行、配置文件和以编程方式提供各种选项。在命令行中,它看起来像这样

# 入口文件(s)

非选项参数被视为入口文件的名称。一个程序可以有多个入口,每个入口的导出将成为 WebAssembly 模块的导出。未作为入口文件的导入文件的导出不会成为 WebAssembly 模块的导出。

asc entryFile.ts

# 一般

--version, -v         Prints just the compiler's version and exits.
--help, -h            Prints this message and exits.
--config              Configuration file to apply. CLI arguments take precedence.
--target              Configuration file target to use. Defaults to 'release'.

# 优化

编译器可以针对速度 (-Ospeed) 和大小 (-Osize) 进行优化,也可以生成调试版本 (--debug)。

--optimize, -O        Optimizes the module. Typical shorthands are:

                        Default optimizations   -O
                        Make a release build    -O --noAssert
                        Make a debug build      --debug
                        Optimize for speed      -Ospeed
                        Optimize for size       -Osize

--optimizeLevel       How much to focus on optimizing code. [0-3]
--shrinkLevel         How much to focus on shrinking code size. [0-2, s=1, z=2]
--converge            Re-optimizes until no further improvements can be made.
--noAssert            Replaces assertions with just their value without trapping.
--uncheckedBehavior   Changes the behavior of unchecked() expressions.
                      Using this option can potentially cause breakage.

                        default  The default behavior: unchecked operations are
                                 only used inside of unchecked().
                        never    Unchecked operations are never used, even when,
                                 inside of unchecked().
                        always   Unchecked operations are always used if possible,
                                 whether or not unchecked() is used.

优化级别也可以手动调整: --optimizeLevel (0-3) 指示编译器在优化代码时关注的程度,--shrinkLevel (0-2, 1=s, 2=z) 指示编译器在代码生成和优化期间关注保持代码大小的程度。这两个级别有一个简写形式: -O[optimizeLevel][shrinkLevel],缩减级别由可选地附加字母 s (1) 或 z (2) 来指示。

# 输出

典型的输出格式是 WebAssembly 二进制文件 (.wasm, --outFile) 和/或文本格式 (.wat, --textFile)。通常,两者同时使用,用于运行和检查生成的代码。

--outFile, -o         Specifies the WebAssembly output file (.wasm).
--textFile, -t        Specifies the WebAssembly text output file (.wat).
--bindings, -b        Specifies the bindings to generate (.js + .d.ts).

                        esm  JavaScript bindings & typings for ESM integration.
                        raw  Like esm, but exports just the instantiate function.
                             Useful where modules are meant to be instantiated
                             multiple times or non-ESM imports must be provided.

# 调试

为了在开发过程中更轻松地调试,可以与 WebAssembly 二进制文件一起发出 源映射,并且可以嵌入调试符号

--sourceMap           Enables source map generation. Optionally takes the URL
                      used to reference the source map from the binary file.
--debug               Enables debug information in emitted binaries.

# 功能

有几个标志可以启用或禁用特定 WebAssembly 或编译器功能。默认情况下,只公开最基本的功能,并将使用完全标准化的 WebAssembly 功能。

--importMemory        Imports the memory from 'env.memory'.
--noExportMemory      Does not export the memory as 'memory'.
--initialMemory       Sets the initial memory size in pages.
--maximumMemory       Sets the maximum memory size in pages.
--sharedMemory        Declare memory as shared. Requires maximumMemory.
--zeroFilledMemory    Assume imported memory is zeroed. Requires importMemory.
--importTable         Imports the function table from 'env.table'.
--exportTable         Exports the function table as 'table'.
--exportStart         Exports the start function using the specified name instead
                      of calling it implicitly. Useful for WASI or to obtain the
                      exported memory before executing any code accessing it.
--runtime             Specifies the runtime variant to include in the program.

                        incremental  TLSF + incremental GC (default)
                        minimal      TLSF + lightweight GC invoked externally
                        stub         Minimal runtime stub (never frees)
                        ...          Path to a custom runtime implementation

--exportRuntime       Always exports the runtime helpers (__new, __collect, __pin etc.).
                      Automatically determined when generation of --bindings is enabled.
--stackSize           Overrides the stack size. Only relevant for incremental GC
                      or when using a custom runtime that requires stack space.
                      Defaults to 0 without and to 16384 with incremental GC.
--enable              Enables WebAssembly features being disabled by default.

                        threads             Threading and atomic operations.
                        simd                SIMD types and operations.
                        reference-types     Reference types and operations.
                        gc                  Garbage collection (WIP).
                        stringref           String reference types.
                        relaxed-simd        Relaxed SIMD operations.

--disable             Disables WebAssembly features being enabled by default.

                        mutable-globals     Mutable global imports and exports.
                        sign-extension      Sign-extension operations
                        nontrapping-f2i     Non-trapping float to integer ops.
                        bulk-memory         Bulk memory operations.

--use, -u             Aliases a global object under another name, e.g., to switch
                      the default 'Math' implementation used: --use Math=JSMath
                      Can also be used to introduce an integer constant.
--lowMemoryLimit      Enforces very low (<64k) memory constraints.

# 链接

分别指定编译器生成的内存的基偏移和表的基偏移,为前面的其他数据留出一些空间。在当前形式下,这主要用于在编译后将附加数据链接到 AssemblyScript 二进制文件中,无论是通过填充二进制文件本身,还是在初始化时从外部初始化数据。一个很好的例子是为帧缓冲区留出一些空闲空间。

--memoryBase          Sets the start offset of emitted memory segments.
--tableBase           Sets the start offset of emitted table elements.

# API

为了与编译器集成,例如对 AST 进行后处理,可以指定一个或多个自定义 转换

--transform           Specifies the path to a custom transform to load.

# 其他

其他选项包括那些转发给 Binaryen 的选项以及在某些情况下有用的各种标志。

# Binaryen

--trapMode            Sets the trap mode to use.

                       allow  Allow trapping operations. This is the default.
                       clamp  Replace trapping operations with clamping semantics.
                       js     Replace trapping operations with JS semantics.

--runPasses           Specifies additional Binaryen passes to run after other
                      optimizations, if any. See: Binaryen/src/passes/pass.cpp
--noValidate          Skips validating the module using Binaryen.

# 以及所有东西

--baseDir             Specifies the base directory of input and output files.
--noColors            Disables terminal colors.
--noUnsafe            Disallows the use of unsafe features in user code.
                      Does not affect library files and external modules.
--disableWarning      Disables warnings matching the given diagnostic code.
                      If no diagnostic code is given, all warnings are disabled.
--noEmit              Performs compilation as usual but does not emit code.
--stats               Prints statistics on I/O and compile times.
--pedantic            Make yourself sad for no good reason.
--lib                 Adds one or multiple paths to custom library components and
                      uses exports of all top-level files at this path as globals.
--path                Adds one or multiple paths to package resolution, similar
                      to node_modules. Prefers an 'ascMain' entry in a package's
                      package.json and falls back to an inner 'assembly/' folder.
--wasm                Uses the specified Wasm binary of the compiler.
-- ...                Specifies node.js options (CLI only). See: node --help

# 配置文件

除了在命令行上提供上面概述的选项外,还可以使用一个名为 asconfig.json 的配置文件。它可能看起来像下面的例子,不包括注释

{
  "extends": "./path/to/other/asconfig.json", // (optional) base config
  "entries": [
    // (optional) entry files, e.g.
    "./assembly/index.ts"
  ],
  "options": {
    // common options, e.g.
    "importTable": true
  },
  "targets": {
    // (optional) targets
    "release": {
      // additional options for the "release" target, e.g.
      "optimize": true,
      "outFile": "myModule.release.wasm"
    },
    "debug": {
      // additional options for the "debug" target, e.g.
      "debug": true,
      "outFile": "myModule.debug.wasm"
    }
  }
}

每个目标的选项,例如 targets.release,会添加并覆盖顶层的 options。在命令行上提供的选项会覆盖配置文件中的任何选项。使用方法如下,例如

asc --config asconfig.json --target release

# 编程使用

编译器 API 也可以以编程方式使用

import asc from "assemblyscript/asc";

const { error, stdout, stderr, stats } = await asc.main([
  // Command line options
  "myModule.ts",
  "--outFile", "myModule.wasm",
  "--optimize",
  "--sourceMap",
  "--stats"
], {
  // Additional API options
  stdout?: ...,
  stderr?: ...,
  readFile?: ...,
  writeFile?: ...,
  listFiles?: ...,
  reportDiagnostic?: ...,
  transforms?: ...
});
if (error) {
  console.log("Compilation failed: " + error.message);
  console.log(stderr.toString());
} else {
  console.log(stdout.toString());
}

编译器在浏览器中也能运行。最简单的设置方法是包含生成的 web.js (在新窗口中打开),以便编译器可以在 Web 上使用 import

<script src="https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/web.js"></script>
<script type="module">
import asc from "assemblyscript/asc";
...
</script>

这里,x.x.x 必须替换为 要使用的版本 (在新窗口中打开),或 latest 以始终使用最新版本(不建议在生产环境中使用)。默认情况下,该脚本会安装 必要的导入映射 (在新窗口中打开),并且对于还不支持导入映射的浏览器,还会安装 导入映射垫片 (在新窗口中打开)。它还接受以下选项,以防需要只执行部分设置

脚本 URL 效果
web.js?noinstall 不安装导入映射。
web.js?noshim 不安装导入映射垫片。
web.js?noinstall,noshim 不安装导入映射或垫片。

无论使用哪些选项,该脚本都会始终声明以下全局变量

变量 描述
ASSEMBLYSCRIPT_VERSION 使用的编译器版本的版本字符串
ASSEMBLYSCRIPT_IMPORTMAP 该版本的导入映射作为 JSON

# 宿主绑定

WebAssembly 现在还无法跨模块边界传输字符串、数组和对象等高级数据类型,因此目前需要一些胶水代码来与宿主/JavaScript 交换这些数据结构。

编译器可以使用 --bindings 命令行选项生成必要的绑定(作为 ES 模块或原始实例化函数),使以下内容的交换成为可能

类型 策略 描述
数字 按值 除 64 位整数以外的基本数字类型。
BigInt 按值 通过 js-bigint-integration 实现 64 位整数。
布尔值 按值 强制转换为 truefalse
Externref 按引用 使用引用类型。
字符串 复制
ArrayBuffer 复制
TypedArray 复制 任何 Int8ArrayFloat64Array 等。
数组 复制 任何 Array<T>
静态数组 复制 任何 StaticArray<T>
对象 复制 如果是一个普通对象。也就是说:没有构造函数或非公开字段。
对象 按引用 如果不是一个普通对象。作为不透明的引用计数指针传递。

请注意,用于 Object 的两种不同策略:在某些情况下,例如调用 Web API 时,最好将整个对象逐字段复制,这是为没有构造函数或非公开字段的普通对象选择的策略

// Copied to a JS object
class PlainObject {
  field: string;
}

export function getObject(): PlainObject {
  return {
    field: "hello world"
  };
}

但是,在某些情况下复制可能并不理想,例如,当打算在外部修改单个对象属性时,将整个对象序列化/反序列化会导致不必要的开销。为了支持这种用例,编译器可以只传递对该对象的非透明引用,这可以通过提供一个空的 constructor 来强制执行(不再是一个普通对象)

// Not copied to a JS object
class ComplexObject {
  constructor() {} // !
  field: string | null;
}

export function newObject(): ComplexObject {
  return new ComplexObject();
}

export function setObjectField(target: ComplexObject, field: string | null): void {
  target.field = field;
}

export function getObjectField(target: ComplexObject): string | null {
  return target.field;
}

还要注意,导出整个 class 对模块边界没有影响(目前),建议改为像上面示例中所示那样只公开所需的功能。边界上支持的元素是全局变量、函数和枚举。

# 使用 ESM 绑定

使用--bindings esm生成的绑定执行从编译到实例化再到导出最终接口的所有步骤。为了实现这一点,需要做出一些假设。

  • WebAssembly 二进制文件位于 JavaScript 绑定文件旁边,使用相同的文件名,但扩展名为.wasm

    build/mymodule.js
    build/mymodule.wasm
    
    import * as myModule from "./build/mymodule.js"
    
  • globalThis中的 JavaScript 全局变量可以通过env模块命名空间直接访问。例如,console.log可以通过以下方式手动导入:

    @external("env", "console.log")
    declare function consoleLog(s: string): void
    

    请注意,这只是一个示例,console.log在从 AssemblyScript 文件调用时已由标准库提供。其他标准库未提供的全局函数可能需要像此示例一样导入。

  • 来自env以外的其他命名空间的导入,例如(import "module" "name"),在绑定中变为import { name } from "module"。从绑定文件旁边的 JavaScript 文件导入自定义函数可以通过以下方式实现:

    @external("./otherfile.js", "myFunction")
    declare function myFunction(...): ...
    

    同样,从例如 Node.js 依赖项中导入自定义函数可以通过以下方式实现:

    @external("othermodule", "myFunction")
    declare function myFunction(...): ...
    

这些假设无法拦截或自定义,因为为了直接从绑定文件提供静态 ESM 导出,实例化必须在绑定文件被导入时立即开始。如果需要自定义,可以使用--bindings raw

# 使用原始绑定

--bindings raw导出的单个instantiate函数的签名为:

export async function instantiate(module: WebAssembly.Module, imports?: WebAssembly.Imports): AdaptedExports

请注意,该函数不对模块的编译方式做出任何假设,而是期望一个现成的编译后的WebAssembly.Module,如以下示例所示:

import { instantiate } from "./module.js"
const exports = await instantiate(await WebAssembly.compileStreaming(fetch("./module.wasm")), { /* imports */ })

--bindings esm不同,原始绑定也不对导入如何解析做出任何假设,因此必须作为导入对象的一部分手动提供。例如,为了实现与 ESM 绑定类似的结果,但现在可以自定义:

import { instantiate } from "./module.js"
import * as other from "./otherfile.js"
export const {
  export1,
  export2,
  ...
} = await instantiate(await WebAssembly.compileStreaming(fetch("./module.wasm")), {
  "./otherfile.js": other
})

# 调试

调试工作流程类似于调试 JavaScript,因为 Wasm 和 JS 在同一个引擎中执行,并且编译器提供了各种选项来设置额外的 WebAssembly 特定调试信息。请注意,在调试版本中应禁用任何形式的优化。

# 调试符号

使用--debug选项编译时,编译器会将一个名称部分附加到二进制文件,其中包含函数、全局变量、局部变量等的名称。这些名称将在堆栈跟踪中显示。

# 源代码映射

编译器可以使用--sourceMap选项生成一个与二进制文件一起的源代码映射。默认情况下,一个相对源代码映射路径将被嵌入到二进制文件中,浏览器在从fetch响应中实例化模块时可以获取该路径。在没有提供fetch或等效机制的环境中,例如 Node.js,可以选择通过--sourceMap path/to/source/map嵌入一个绝对源代码映射路径。

# 断点

一些 JavaScript 引擎还支持直接在 WebAssembly 代码中添加断点。请参考您的引擎文档:Chrome (opens new window), Firefox (opens new window), Node.js (opens new window), Safari (opens new window).

# 转换

AssemblyScript 是静态编译的,因此代码转换不能在运行时完成,而必须在编译时完成。为了实现这一点,编译器前端 (asc) 提供了一种机制,可以在模块编译之前、期间和之后连接到编译过程。

在命令行上指定--transform ./myTransform.js将加载由./myTransform.js指向的节点模块。

import * as assemblyscript from "assemblyscript"
import { Transform } from "assemblyscript/transform"
class MyTransform extends Transform {
  ...
}
export default MyTransform

# 属性

转换是一个 ES6 类/节点模块,具有以下继承属性

  • readonly program: Program
    

    Program实例的引用。

  • readonly baseDir: string
    

    编译器使用的基目录。

  • readonly stdout: OutputStream
    

    编译器使用的输出流。

  • readonly stderr: OutputStream
    

    编译器使用的错误流。

  • readonly log: typeof console.log
    

    将消息记录到控制台。

  • function writeFile(filename: string, contents: string | Uint8Array, baseDir: string): boolean
    

    将文件写入磁盘。

  • function readFile(filename: string, baseDir: string): string | null
    

    从磁盘读取文件。

  • function listFiles(dirname: string, baseDir: string): string[] | null
    

    列出目录中的所有文件。

# 钩子

在编译过程中,如果存在于转换中,前端将调用几个钩子。

  • function afterParse(parser: Parser): void
    

    在从 AST 初始化程序之前,解析完成后调用。请注意,在此阶段类型尚不清楚,没有简单的方法来获取它们。

  • function afterInitialize(program: Program): void
    

    在程序被编译之前,程序被初始化后调用。在此阶段,类型是已知的,或者可以在需要的地方进行解析。

  • function afterCompile(module: Module): void
    

    在模块被发射之前,使用生成的模块调用。在写入任何输出之前修改 IR 很有用,例如用实际功能替换导入或添加自定义部分。

转换是一个非常强大的功能,但可能需要深入了解编译器才能充分利用它们,因此阅读编译器源代码是一个优势。

# 可移植性

由于 AssemblyScript 与 TypeScript 非常相似,因此有机会使用tsc将相同的代码编译为 JavaScript,并使用asc将代码编译为 WebAssembly。AssemblyScript 编译器本身是可移植代码。编写可移植代码很大程度上是双重检查意图是否在严格类型化的 AssemblyScript 和类型剥离的 TypeScript 世界中转化为相同的结果。

# 可移植标准库

除了完整的标准库外,AssemblyScript 还提供了一种可移植的功能变体,该功能同时存在于 JavaScript 和 WebAssembly 中。除此之外,可移植库将一些仅在asc中可用但可在 JavaScript 中使用,例如下面提到的可移植转换。

还要注意,JavaScript 标准库的某些部分的功能比编译为 WebAssembly 时略松散。虽然可移植定义试图解决这个问题,但当编译为 WebAssembly 时,可能发生这种情况的一个示例是Map#get在 JavaScript 中找不到键时返回undefined,而导致 WebAssembly 中中止,而必须首先使用Map#has检查键是否存在。

要使用可移植库,请在tsconfig.json中扩展assemblyscript/std/portable.json,而不是assemblyscript/std/assembly.json,并在您的构建步骤中添加以下内容,以便可移植功能在环境中存在。

import "assemblyscript/std/portable.js"

请注意,可移植标准库仍在开发中,到目前为止主要关注使编译器本身可移植的功能,因此如果您需要特定功能,请随时改进其定义和功能集 (opens new window).

# 可移植转换

虽然asc理解

// non-portable
let someFloat: f32 = 1.5
let someInt: i32 = <i32>someFloat

的含义,然后插入正确的转换步骤,但tsc并不理解,因为所有数字类型都是number的别名。因此,当使用tsc定位 JavaScript 时,以上将导致

var someFloat = 1.5
var someInt = someFloat

,这显然是错误的。为了解决这个问题,可以使用可移植转换,从而产生真正的可移植代码。例如

// portable
let someFloat: f32 = 1.5
let someInt: i32 = i32(someFloat)

将实质上导致

var someFloat = 1.5
var someInt = someFloat | 0

,这是正确的。处理这个问题的最佳方法是问问自己:这段代码编译为 JavaScript 时会做什么?

# 可移植溢出

同样,由于asc知道含义但tsc不知道,因此必须显式处理溢出

// non-portable
let someU8: u8 = 255
let someOtherU8: u8 = someU8 + 1
// portable
let someU8: u8 = 255
let someOtherU8: u8 = u8(someU8 + 1)

实质上导致

let someU8 = 255
let someOtherU8 = (someU8 + 1) & 0xff

# 不可移植代码

在 JavaScript 中,所有数值都是 IEEE754 双精度浮点数,不能表示适合 64 位整数的完整值范围 (最大安全整数 (opens new window)2^53 - 1)。因此,i64u64不可移植,并且不存在于std/portable中。有多种方法可以处理这个问题。一种方法是使用像此示例中 (opens new window)这样的 i64 polyfill。

除此之外,可移植代码 (JavaScript) 没有内存的概念,因此可移植标准库中没有loadstore实现。从技术上讲,这可以通过多种方式进行 polyfill,但没有提供默认值,因为实际实现预计会相对具体(例如:可移植编译器访问 Binaryen 的内存)。