Arcjet bundles WebAssembly with our security as code SDK. This helps developers implement common security functionality like PII detection and bot detection directly in their code. Much of the logic is embedded in Wasm, which gives us a secure sandbox with near-native performance and is part of our philosophy around local-first security.
The ability to run the same code across platforms is also helpful as we build out support from JavaScript to other tech stacks, but it requires an important abstraction to translate between languages (our Wasm is compiled from Rust).
The WebAssembly Component Model is the powerful construct which enables this, but a construct can only be as good as the implementations and tooling surrounding it. For the Component Model, this is most evident in the code generation for Hosts (environments that execute WebAssembly Component Model) and Guests (WebAssembly modules written in any language and compiled to the Component Model; Rust in our case).
The Component Model defines a language for communication between Hosts and Guests which is primarily composed of types, functions, imports and exports. It tries to define a broad language, but some types, such as variants, tuples, and resources, might not exist in a given general purpose programming language.
When a tool tries to generate code for one of these languages, the authors often need to get creative to map Component Model types to that general purpose language. For example, we use jco for generating JS bindings and this implements variants using a JavaScript object in the shape of { tag: string, value: string }. It even has a special case for the result<_, _> type where the error variant is turned into an Error and thrown.
This post explores how the Wasm Component Model enables cross-language integrations, the complexities of code generation for Hosts and Guests, and the trade-offs we make to achieve idiomatic code in languages like Go.
Host code generation for Go
At Arcjet, we have had to build a tool to generate code for Hosts written in the Go programming language. Although our SDK attempts to analyze everything locally, that is not always possible and so we have an API written in Go which augments local decisions with additional metadata.
Go has a very minimal syntax and type system by design. They didn’t even have generics until very recently and they still have significant limitations. This makes codegen from the Component Model to Go complex in various ways.
For example, we could generate a result<_, _> as:
type Result[V any] struct { value V err error }
However, this limits the type that can be provided in the error position. So we’d need to codegen it as:
type Result[V any] struct { value V err error }
This works but becomes cumbersome to use with other idiomatic Go, which often uses the val, err := doSomething() convention to indicate the same semantics as the Result type we’ve defined above.
Additionally, constructing this Result is cumbersome: Result[int, string]{value: 1, err: ""}. Instead of providing the Result type, we probably want to match idiomatic patterns so Go users feel natural consuming our generated bindings.
Idiomatic vs Direct Mapping
Code can be generated to feel more natural to the language or it can be a more direct mapping to the Component Model types. Neither option fits 100% of use cases so it is up to the tool authors to decide which makes the most sense.
For the Arcjet tooling, we chose the idiomatic Go approach for option<_> and result<_, _> types, which map to val, ok := doSomething() and val, err := doSomething() respectively. For variants, we create an interface that each variant needs to implement, such as:
type Result[V any, E any] struct { value V err E }
This strikes a good balance between type safety and unnecessary wrapping. Of course, there are situations where the wrapping is required, but those can be handled as edge cases.
Developers may struggle with non-idiomatic patterns, leading to verbose, less maintainable code. Using established conventions makes the code feel more familiar, but does require some additional effort to implement.
We decided to take the idiomatic path to minimize friction and make it easier for our team so we know what to expect when moving around the codebase.
Calling conventions
One of the biggest decisions tooling authors need to make is the calling convention of the bindings. This includes deciding how/when imports will be compiled, if the Wasm module will be compiled during setup or instantiation, and cleanup.
In the Arcjet codebase, we chose the factory/instance pattern to optimize performance. Compiling a WebAssembly module is expensive, so we do it once in the NewBotFactory() constructor. Subsequent Instantiate() calls are then fast and cheap, allowing for high throughput in production workloads.
type BotConfig interface { isBotConfig() } func (AllowedBotConfig) isBotConfig() {} func (DeniedBotConfig) isBotConfig() {}
Consumers construct this BotFactory once by calling NewBotFactory(ctx) and use it to create multiple instances via the Instantiate method.
func NewBotFactory( ctx context.Context, ) (*BotFactory, error) { runtime := wazero.NewRuntime(ctx) // ... Imports are compiled here if there are any // Compiling the module takes a LONG time, so we want to do it once and hold // onto it with the Runtime module, err := runtime.CompileModule(ctx, wasmFileBot) if err != nil { return nil, err } return &BotFactory{runtime, module}, nil }
Instantiation is very fast if the module has already been compiled, like we do with runtime.CompileModule() when constructing the factory.
The BotInstance has functions which were exported from the Component Model definition.
func (f *BotFactory) Instantiate(ctx context.Context) (*BotInstance, error) { if module, err := f.runtime.InstantiateModule(ctx, f.module, wazero.NewModuleConfig()); err != nil { return nil, err } else { return &BotInstance{module}, nil } }
Generally, after using a BotInstance, we want to clean it up to ensure we’re not leaking memory. For this we provide the Close function.
func (i *BotInstance) Detect( ctx context.Context, request string, options BotConfig, ) (BotResult, error) { // ... Lots of generated code for binding to Wazero }
If you want to clean up the entire BotFactory, that can be closed too:
type Result[V any] struct { value V err error }
We can put all these APIs together to call functions on this WebAssembly module:
type Result[V any, E any] struct { value V err E }
This pattern of factory and instance construction takes more code to use, but it was chosen to achieve as much performance as possible in the hot paths of the Arcjet service.
By front-loading the compilation cost, we ensure that in the hot paths of the Arcjet service - where latency matters most - request handling is as efficient as possible. This trade-off does add some complexity to initialization code, but it pays off with substantially lower overhead per request - see our discussion of the tradeoffs.
Trade-offs
Any time we need to integrate two or more languages, it is fraught with trade-offs that need to be made—whether using native FFI or the Component Model.
This post discussed a few of the challenges we’ve encountered at Arcjet and the reasoning behind our decisions. If we all build on the same set of primitives, such as the Component Model and WIT, we can all leverage the same set of high-quality primitives, such as wit-bindgen or wit-component, and build tooling to suit every use case. This is why working towards standards helps everyone.
The WebAssembly Component Model offers a powerful abstraction for cross-language integration, but translating its types into languages like Go introduces subtle design challenges. By choosing idiomatic patterns and selectively optimizing for performance - such as using a factory/instance pattern - we can provide a natural developer experience while maintaining efficiency.
As tooling around the Component Model evolves, we can look forward to more refined codegen approaches that further simplify these integrations.
The above is the detailed content of The Wasm Component Model and idiomatic codegen. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

Effective handling of JSON in Go requires attention to structural labels, optional fields and dynamic analysis. Use the struct tag to customize the JSON key name, such as json:"name"; make sure the fields are exported for access by the json package. Use pointers or omitempty tags when processing optional fields to distinguish between unprovided values ??from explicit zeros. When parsing unknown JSON, map[string]interface{} can be used to extract data with type assertions. The default number will be parsed as float64. json.MarshalIndent can be used to beautify the output during debugging, but the production environment should avoid unnecessary formatting. Mastering these techniques can improve the robustness and ability of your code

Go programs can indeed interact with C code through Cgo, which allows Go to call C functions directly. When using Cgo, just import the pseudo-package "C" and embed C code in the comments above the import line, such as including C function definitions and calling them. In addition, external C library can be linked by specifying link flags such as #cgoLDFLAGS. However, there are many issues to pay attention to when using Cgo: 1. Memory management needs to be processed manually and cannot rely on Go garbage collection; 2. Go types may not match C types, and types such as C.int should be used to ensure consistency; 3. Multiple goroutine calls to non-thread-safe C libraries may cause concurrency problems; 4. There is performance overhead for calling C code, and the number of calls across language boundaries should be reduced. Cgo's lack

Yes,Goapplicationscanbecross-compiledfordifferentoperatingsystemsandarchitectures.Todothis,firstsettheGOOSandGOARCHenvironmentvariablestospecifythetargetOSandarchitecture,suchasGOOS=linuxGOARCH=amd64foraLinuxbinaryorGOOS=windowsGOARCH=arm64foraWindow

Go simplifies the use of pointers and improves security. 1. It does not support pointer arithmetic to prevent memory errors; 2. Automatic garbage collection and management of memory without manual allocation or release; 3. The structure method can seamlessly use values ??or pointers, and the syntax is more concise; 4. Default safe pointers to reduce the risk of hanging pointers and memory leakage. These designs make Go easier to use and safer than C/C, but sacrifice some of the underlying control capabilities.

Go compiles the program into a standalone binary by default, the main reason is static linking. 1. Simpler deployment: no additional installation of dependency libraries, can be run directly across Linux distributions; 2. Larger binary size: Including all dependencies causes file size to increase, but can be optimized through building flags or compression tools; 3. Higher predictability and security: avoid risks brought about by changes in external library versions and enhance stability; 4. Limited operation flexibility: cannot hot update of shared libraries, and recompile and deployment are required to fix dependency vulnerabilities. These features make Go suitable for CLI tools, microservices and other scenarios, but trade-offs are needed in environments where storage is restricted or relies on centralized management.

Goensuresmemorysafetywithoutmanualmanagementthroughautomaticgarbagecollection,nopointerarithmetic,safeconcurrency,andruntimechecks.First,Go’sgarbagecollectorautomaticallyreclaimsunusedmemory,preventingleaksanddanglingpointers.Second,itdisallowspointe

To create a buffer channel in Go, just specify the capacity parameters in the make function. The buffer channel allows the sending operation to temporarily store data when there is no receiver, as long as the specified capacity is not exceeded. For example, ch:=make(chanint,10) creates a buffer channel that can store up to 10 integer values; unlike unbuffered channels, data will not be blocked immediately when sending, but the data will be temporarily stored in the buffer until it is taken away by the receiver; when using it, please note: 1. The capacity setting should be reasonable to avoid memory waste or frequent blocking; 2. The buffer needs to prevent memory problems from being accumulated indefinitely in the buffer; 3. The signal can be passed by the chanstruct{} type to save resources; common scenarios include controlling the number of concurrency, producer-consumer models and differentiation

Go is ideal for system programming because it combines the performance of compiled languages ??such as C with the ease of use and security of modern languages. 1. In terms of file and directory operations, Go's os package supports creation, deletion, renaming and checking whether files and directories exist. Use os.ReadFile to read the entire file in one line of code, which is suitable for writing backup scripts or log processing tools; 2. In terms of process management, the exec.Command function of the os/exec package can execute external commands, capture output, set environment variables, redirect input and output flows, and control process life cycles, which are suitable for automation tools and deployment scripts; 3. In terms of network and concurrency, the net package supports TCP/UDP programming, DNS query and original sets.
