iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🌟

WebAssembly GC Proposal: What It Is and Where It's Headed

に公開

Introduction

This is not an article to introduce GC just because it's being added to WebAssembly. Rather, it is an article to provide a reality check for those dreaming that GC support will make Wasm compile sizes smaller and more lightweight for any language, by showing just how far away the adoption of WebAssembly GC really is.

The WebAssembly GC Proposal (Team) is proceeding with specification development by splitting the necessary parts, and it will likely take several years before GC actually becomes operational. My personal intuition is that it's a 50/50 chance whether GC will be adopted in the future.

However, the specifications derived from the GC Proposal are all meaningful even if GC itself is not adopted, so I would like to introduce them in this article.

Refer basically to this link:

https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md

Disclaimer

I have limited experience in low-level programming and only started studying the relevant specifications for the sake of WebAssembly, so I believe there may be various mistakes in this article.

My current status is roughly at the point of having tried hand-written wat (WebAssembly Text Format), written a simple compiler, and performed Binary Encoding to get a feel for it.

Purpose of Introducing GC in WebAssembly

https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md#motivation

  • High-speed execution
  • Small module sizes
  • Because implementation of many languages assumes it

Wasm's requirements for GC:

  • Allocation and destruction of structures
  • Management of objects shared with the outside world, such as JavaScript

The goal is to perform these safely, quickly, without relying on external specifications, and in a thread-safe manner.

Approach

This is not intended to affect all existing Wasm, but is positioned as an extension.

  • Add a heap memory independent of the linear memory (which WebAssembly currently has)
  • Target common data structures such as tuples, vectors, and unboxed scalars
  • Allow for some dynamic overhead to simplify implementation
  • Add specifications to make runtime type information explicit

Explanation with a Fictional Language

Suppose there is a fictional (TS-like) language like this:

type tup = (int, int, bool)
type vec3d = float[3]
type buf = {var pos : int, chars : char[]}

function f() {
  let t : tup = (1, 2, true)
  t.1
}

function g() {
  let v : vec3d = [1, 1, 5]
  v[1]
}

function h() {
  let b : nullable buf = {pos = 0, chars = "AAAA"}
  b.buf[b.pos]
}

Using wat (WebAssembly Text Format) and the GC Extension, this would be expressed as follows:

(type $tup (struct i64 i64 i32))
(type $vec3d (array (mut f64)))
(type $char-array (array (mut i8)))
(type $buf (struct (field $pos (mut i64)) (field $chars (ref $char-array))))

(func $f
  (struct.new $tup (i64.const 1) (i64.const 2) (i64.const 1))
  (let (local $t (ref $tup))
    (struct.get $tup 1 (local.get $t))
    (drop)
  )
)

(func $g
  (array.new $vec3d (i32.const 3) (f64.const 1))
  (let (local $v (ref $vec3d))
    (array.set $vec3d (local.get $v) (i32.const 2) (i32.const 5))
    (array.get $vec3d (local.get $v) (i32.const 1))
    (drop)
  )
)

(func $h
  (local $b (optref $buf))
  (local.set $b
    (struct.new $buf
      (i64.const 0)
      (array.new $char-array (i32.const 4) (i32.const 0x41))
    )
  )
  (array.get $buf
    (struct.get $buf $chars (local.get $b))
    (struct.get $buf $pos (local.get $b))
  )
  (drop)
)

(type $tup (struct i64 i64 i32))

Declares a structure with a struct declaration.

(struct.new $tup (i64.const 1) (i64.const 2) (i64.const 1))

Creates a structure using struct.new. This is allocated in the heap memory.

function f() {
  let t : tup = (1, 2, true)
  t.1
}

Although not explicitly stated in the documentation, within this function, the reference to t likely disappears once t.1 (=2) is returned, so it probably becomes a target for GC. I get the impression that ref.cast downcasting might also be used to release references to unused structure members.

Looking at the discussions, there is still little mention of the actual behavior of the heap, as if they are saying "let's discuss the heap's actual behavior after other specifications are settled."

At this point, it feels quite like a high-level language.

Challenges: A Significant Leap from the Current Specification

This proposal represents a significant leap from the current specification.

First, the current Wasm specification can only handle things like i32, i64, f32, f64, and externref (external reference pointers).

In order to introduce these GC semantics, the following specifications must be established:

  • Immutable fields
  • Reference types
  • Downcasting
  • Dynamic linking
  • Subtyping

After these are decided, it becomes possible to declare structures on the heap, such as struct. Before considering the semantics of heap memory, it is necessary to advance the specifications for structures within wat and the specifications for handling them in functions.

Therefore, from here on, it has been divided into a large number of specifications, or existing proposals have come to be discussed as specifications by giving them a GC-based interpretation. That is the story so far.

Proposal: Reference Type

https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md

A specification for passing around pointers received from the outside within Wasm.

Currently, it only allows for knowing the pointer, so it doesn't mean the contents can be manipulated.

For example, while complex structures like document.body cannot (currently) be passed to Wasm, you can pass them around knowing just that they are some kind of DOM reference, and delegate them between imported/exported functions for processing.

If Wasm could know what structure an externref has, it would eventually be able to access its internals; this has been split into the Interface Type specification, which defines what structure a reference has.

externref has already been adopted and implementation has begun. In Rust's wasm-bindgen, enabling an option has successfully reduced glue code.

Proposal: Interface Type

https://github.com/WebAssembly/interface-types/blob/main/proposals/interface-types/Explainer.md

A specification that defines structures for values other than i32 primitives, making them manageable. Apparently, a sense of concern arose because the interfaces were riddled with i32s when the WASI interface was being decided.

It is stated that most of the calling overhead between Wasm and JS occurs in glue code because these interfaces are not properly defined, so defining them here has the potential to solve this problem.

https://github.com/WebAssembly/interface-types/blob/main/proposals/interface-types/Explainer.md#optimizing-calls-to-web-apis

In this specification, you will be able to define interfaces for modules and functions accepted from the outside.

(adapter_module
  (import "duplicate" (adapter_func $dup (param string) (result string string)))
  (import "print" (adapter_func $print (param string)))
  (adapter_func (export "print_twice") (param string)
    call_adapter $dup
    call_adapter $print
    call_adapter $print
  )
)

Functions provided by the adapter are called using call_adapter instead of call.

(adapter_func $return_one_of (param string string i32) (result string)
  i32.eqz
  (if (param string string) (result string)
    (then return)
    (else drop return))
)

It seems that adapter_func is the implementation for adapter_module.

I haven't quite grasped it yet, so I'll omit the details. For more information, see here: https://github.com/WebAssembly/interface-types/blob/main/proposals/interface-types/Explainer.md#adapter-modules

The following section describes what happens when you write an interface for libc using this specification.

https://github.com/WebAssembly/interface-types/blob/main/proposals/interface-types/Explainer.md#adapter-fusion

Instead of .wat, the interface description is expressed in a file called .wit, and there was a Rust code generator (wit-bindgen) in the bytecodealliance repository that uses this.

https://github.com/bytecodealliance/wit-bindgen

(Looking at the discussions, the specifications change frequently, so these samples will likely not be implemented exactly as shown.)

(Looking at these specifications, I can see that each group for GC and Interface Types is independently considering array and list structures.)

This specification depends on the module linking specification.

Proposal: Type Imports

https://github.com/WebAssembly/proposal-type-imports/blob/master/proposals/type-imports/Overview.md

A specification to allow importing type definitions from outside Wasm.

(import "file" "File" (type $File any))
(import "file" "open" (func $open (param $name i32) (result (ref $File))))
(import "file" "read_byte" (func $read (param (ref $File)) (result i32)))
(import "file" "close" (func $close (param (ref $File))))

Until now, only functions and memory could be imported externally, but this allows for receiving type definition interfaces. In addition to type, heap_type semantics have also been proposed for GC.

Derived from this, WebAssembly.Type for implementing type definitions from the JS interface side has also been proposed. Looking at meeting notes, discussions often take place on how to align this with TC39 semantics.

Proposal: Module Linking

https://github.com/WebAssembly/module-linking

This is a specification for dynamic linking with Wasm or others (JS). While current .wasm files operate as standalone files and require JS glue code for imports/exports, this proposal aims to enable direct resolution between .wasm files and is also considering bindings with ES Modules.

This specification depends on Interface Type, and the adapter module makes an appearance here as well.

(adapter module
  (adapter module $A
    (module $B
      (func (export "one") (result i32) (i32.const 1))
    )
  )
  (module $C
    (func (export "two") (result i32) (i32.const 2))
  )
)

The instructions already vary between adapter_module and adapter module, but looking at the meeting notes, adapter module seems to be the latest version.

There is also a proposal for inner modules, which allows defining a module within another module.

(adapter module
  (module $Inner ...)
  (instance $left (instantiate $Inner))
  (instance $right (instantiate $Inner))
  (instance $pair
    (export "left" (instance $left))
    (export "right" (instance $right))
  )
)

JS Interface

import Foo from "./foo.wasm" as "wasm-module";
assert(Foo instanceof WebAssembly.Module);

This involves importing by explicitly stating that it is Wasm, using the as or assert syntax currently being proposed for ESM Import.

What GC does using these specifications

My Personal Take

When reading .wat files, you'll notice that the atmosphere is different from the low-level code that manages the stack of linear memory in conventional wat; it feels more like an S-expression representation of a high-level language.

Indeed, I strongly feel the goal is to reduce build sizes from other languages by providing representations tailored to high-level languages, moving away from the overly primitive Wasm code of today. However, in reality, behaviors depend on the GC characteristics and runtime metadata of each language, so it's hard to imagine a straightforward 1:1 conversion. Personally, I believe it will likely be written in subsets of existing languages with some features removed, or in specialized languages focused on this specification.

As a concern, since the specification is expanding, if implemented literally, the current image of Wasm—where "it remains neutral across browsers and various runtimes because the spec is simple"—might disappear, and characteristics dependent on the execution engine's runtime may become more prominent.

To avoid this, I personally think this specification needs to demonstrate benefits that outweigh its complexity. Specifically, something like a compiler where a simple Hello World in Java, .NET, or Go currently becomes 10+ MB, but would drop to 100 KB using this specification...

At this point, I believe that to create lightweight modules in Wasm, one has no choice but to use hand-written .wat or build Rust wasm32-unknown-unknown with no_std and without external modules. That's why I've been working on things like a hand-written wat-playground.

https://wat-playground.netlify.app/

Discussion