Arcjet には、WebAssembly と Security as Code SDK がバンドルされています。これは、開発者が PII 検出やボット検出などの一般的なセキュリティ機能をコードに直接実装するのに役立ちます。ロジックの多くは Wasm に埋め込まれており、ネイティブに近いパフォーマンスを備えた安全なサンドボックスを提供し、ローカルファースト セキュリティに関する私たちの哲学の一部です。
プラットフォーム間で同じコードを実行できる機能も、JavaScript から他の技術スタックへのサポートを構築する際に役立ちますが、言語間で変換するには重要な抽象化が必要です (Wasm は Rust からコンパイルされています)。
WebAssembly コンポーネント モデル はこれを可能にする強力な構造ですが、その構造はそれを囲む実装とツールによってのみ優れています。コンポーネント モデルの場合、これはホスト (WebAssembly コンポーネント モデルを実行する環境) とゲスト (任意の言語で記述され、コンポーネント モデルにコンパイルされた WebAssembly モジュール。この場合は Rust) のコード生成で最も明らかです。
コンポーネント モデルは、主に型、関数、インポート、エクスポートで構成されるホストとゲスト間の通信用の言語を定義します。幅広い言語を定義しようとしていますが、バリアント、タプル、リソースなどの一部の型は、特定の汎用プログラミング言語に存在しない可能性があります。
ツールがこれらの言語のいずれかのコードを生成しようとする場合、作成者は多くの場合、コンポーネント モデル タイプをその汎用言語にマッピングするために創造性を発揮する必要があります。たとえば、JS バインディングの生成には jco を使用します。これにより、{ タグ: 文字列、値: 文字列 } の形式の JavaScript オブジェクトを使用してバリアントが実装されます。結果には特殊なケースもあります<_, _>エラーのバリアントが Error に変換されてスローされるタイプ。
この投稿では、Wasm コンポーネント モデルが言語間の統合をどのように可能にするか、ホストとゲストのコード生成の複雑さ、Go などの言語で慣用的なコードを実現するために行うトレードオフについて説明します。
Arcjet では、Go プログラミング言語で書かれたホスト用のコードを生成するツールを構築する必要がありました。私たちの SDK はすべてをローカルで分析しようとしますが、それが常に可能であるとは限らないため、Go で書かれた API を用意し、追加のメタデータでローカルの決定を強化します。
Go は設計上、非常に最小限の構文と型システムを備えています。ごく最近までジェネリック医薬品さえありませんでしたが、依然として重大な制限があります。これにより、コンポーネント モデルから Go へのコード生成がさまざまな点で複雑になります。
たとえば、次のような結果を生成できます<_, _>として:
type Result[V any] struct { value V err error }
ただし、エラー位置に提供できる型が制限されます。したがって、次のようにコード生成する必要があります:
type Result[V any] struct { value V err error }
これは機能しますが、他の慣用的な Go で使用すると面倒になります。Go では、上で定義した Result タイプと同じセマンティクスを示すために val, err := doSomething() 規則がよく使用されます。
さらに、この Result の構築は面倒です: Result[int, string]{value: 1, err: ""}。 Result タイプを提供する代わりに、Go ユーザーが生成されたバインディングを自然に利用できるように、慣用的なパターンに一致させたいと考えています。
コードは、言語にとってより自然に感じられるように生成することも、コンポーネント モデル タイプへのより直接的なマッピングにすることもできます。どちらのオプションも 100% のユースケースに適合するわけではないため、どちらが最も合理的であるかを判断するのはツールの作成者次第です。
Arcjet ツールの場合、オプションとして慣用的な Go アプローチを選択しました<_>結果<_, _>これらの型は、それぞれ val, ok := doSomething() および val, err := doSomething() にマップされます。バリアントの場合、各バリアントが実装する必要がある次のようなインターフェイスを作成します。
type Result[V any, E any] struct { value V err E }
これにより、型の安全性と不必要なラッピングのバランスが取れています。もちろん、ラッピングが必要な状況もありますが、それはエッジケースとして処理できます。
開発者は、非慣用的なパターンに苦労し、冗長で保守性の低いコードにつながる可能性があります。確立された規則を使用すると、コードがより親しみやすくなりますが、実装には追加の作業が必要になります。
コードベースを移動する際に何が起こるかを理解できるように、摩擦を最小限に抑え、チームにとって作業を容易にする慣用的な方法を採用することにしました。
ツール作成者が行う必要がある最大の決定の 1 つは、バインディングの呼び出し規約です。これには、インポートをいつどのようにコンパイルするか、Wasm モジュールをセットアップ中またはインスタンス化中、およびクリーンアップ中にコンパイルするかどうかの決定が含まれます。
Arcjet コードベースでは、パフォーマンスを最適化するためにファクトリ/インスタンス パターンを選択しました。 WebAssembly モジュールのコンパイルにはコストがかかるため、NewBotFactory() コンストラクターで 1 回だけ実行します。後続の Instantiate() 呼び出しは高速かつ安価であり、実稼働ワークロードでの高スループットが可能になります。
type BotConfig interface { isBotConfig() } func (AllowedBotConfig) isBotConfig() {} func (DeniedBotConfig) isBotConfig() {}
消費者は NewBotFactory(ctx) を呼び出してこの BotFactory を 1 回構築し、それを使用して Instantiate メソッド経由で複数のインスタンスを作成します。
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 }
ファクトリを構築するときに runtime.CompileModule() を使用するように、モジュールがすでにコンパイルされている場合、インスタンス化は非常に高速です。
BotInstance には、コンポーネント モデル定義からエクスポートされた関数があります。
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 } }
通常、BotInstance を使用した後は、メモリ リークが発生していないことを確認するために、BotInstance をクリーンアップする必要があります。このために、Close 関数を提供します。
func (i *BotInstance) Detect( ctx context.Context, request string, options BotConfig, ) (BotResult, error) { // ... Lots of generated code for binding to Wazero }
BotFactory 全体をクリーンアップしたい場合は、それを閉じることもできます:
type Result[V any] struct { value V err error }
これらの API をすべてまとめて、この WebAssembly モジュールの関数を呼び出すことができます。
type Result[V any, E any] struct { value V err E }
このファクトリーとインスタンスの構築パターンでは使用するコードが多くなりますが、Arcjet サービスのホット パスで可能な限り多くのパフォーマンスを達成するために選択されました。
コンパイル コストを前倒しすることで、Arcjet サービスのホット パス (レイテンシーが最も重要な場所) でのリクエスト処理が可能な限り効率的になるようにします。このトレードオフにより、初期化コードは多少複雑になりますが、リクエストごとのオーバーヘッドが大幅に減少するというメリットがあります。トレードオフについての説明を参照してください。
2 つ以上の言語を統合する必要がある場合は常に、ネイティブ FFI を使用する かコンポーネント モデルを使用するかにかかわらず、トレードオフを行う必要があります。
この投稿では、アークジェットで遭遇したいくつかの課題と、その決定の背後にある理由について説明しました。コンポーネント モデルや WIT などの同じプリミティブのセットを全員が構築すると、wit-bindgen や wit-component などの高品質のプリミティブの同じセットを活用できます。 を使用して、あらゆるユースケースに適したツールを構築します。これが、標準に向けた取り組みがすべての人に役立つ理由です。
WebAssembly コンポーネント モデルは、言語間の統合のための強力な抽象化を提供しますが、その型を Go などの言語に変換すると、微妙な設計上の課題が生じます。ファクトリ/インスタンス パターンの使用など、慣用的なパターンを選択し、パフォーマンスを選択的に最適化することで、効率を維持しながら自然な開発者エクスペリエンスを提供できます。
コンポーネント モデルに関するツールが進化するにつれて、これらの統合をさらに簡素化する、より洗練されたコード生成アプローチが期待できます。
以上がWasm コンポーネント モデルと慣用的な codegenの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。