Arcjet bündelt WebAssembly mit unserem Security-as-Code-SDK. Dies hilft Entwicklern, gängige Sicherheitsfunktionen wie PII-Erkennung und Bot-Erkennung direkt in ihren Code zu implementieren. Ein Großteil der Logik ist in Wasm eingebettet, was uns eine sichere Sandbox mit nahezu nativer Leistung bietet und Teil unserer Philosophie rund um Local-First-Sicherheit ist.
Die Möglichkeit, denselben Code plattformübergreifend auszuführen, ist ebenfalls hilfreich, da wir die Unterstützung von JavaScript auf andere Tech-Stacks ausbauen, aber für die Übersetzung zwischen Sprachen ist eine wichtige Abstraktion erforderlich (unser Wasm wird aus Rust kompiliert).
Das WebAssembly Component Model ist das leistungsstarke Konstrukt, das dies ermöglicht, aber ein Konstrukt kann nur so gut sein wie die Implementierungen und Tools, die es umgeben. Beim Komponentenmodell zeigt sich dies am deutlichsten in der Codegenerierung für Hosts (Umgebungen, die das WebAssembly-Komponentenmodell ausführen) und Gäste (WebAssembly-Module, die in einer beliebigen Sprache geschrieben und zum Komponentenmodell kompiliert wurden; in unserem Fall Rust).
Das Komponentenmodell definiert eine Sprache für die Kommunikation zwischen Hosts und Gästen, die hauptsächlich aus Typen, Funktionen, Importen und Exporten besteht. Es wird versucht, eine breite Sprache zu definieren, aber einige Typen, wie Varianten, Tupel und Ressourcen, existieren möglicherweise nicht in einer bestimmten Programmiersprache für allgemeine Zwecke.
Wenn ein Tool versucht, Code für eine dieser Sprachen zu generieren, müssen die Autoren oft kreativ werden, um Komponentenmodelltypen dieser Allzwecksprache zuzuordnen. Zum Beispiel verwenden wir jco zum Generieren von JS-Bindungen und dies implementiert Varianten mithilfe eines JavaScript-Objekts in der Form { tag: string, value: string }. Es gibt sogar einen Sonderfall für das Ergebnis <_, _> Geben Sie ein, bei dem die Fehlervariante in einen Fehler umgewandelt und ausgelöst wird.
In diesem Beitrag wird untersucht, wie das Wasm-Komponentenmodell sprachübergreifende Integrationen ermöglicht, wie komplex die Codegenerierung für Hosts und Gäste ist und welche Kompromisse wir eingehen, um idiomatischen Code in Sprachen wie Go zu erreichen.
Bei Arcjet mussten wir ein Tool entwickeln, um Code für Hosts zu generieren, der in der Programmiersprache Go geschrieben wurde. Obwohl unser SDK versucht, alles lokal zu analysieren, ist das nicht immer möglich und deshalb haben wir eine in Go geschriebene API, die lokale Entscheidungen mit zusätzlichen Metadaten ergänzt.
Go hat von Natur aus ein sehr minimales Syntax- und Typsystem. Bis vor kurzem gab es noch nicht einmal Generika, und es gibt immer noch erhebliche Einschränkungen. Dies macht die Codegenerierung vom Component Model to Go in vielerlei Hinsicht komplex.
Zum Beispiel könnten wir ein Ergebnis generieren<_, _> als:
type Result[V any] struct { value V err error }
Dies schränkt jedoch den Typ ein, der an der Fehlerposition bereitgestellt werden kann. Wir müssten es also wie folgt codegenerieren:
type Result[V any] struct { value V err error }
Dies funktioniert, wird aber mit anderen idiomatischen Go-Elementen umständlich zu verwenden, da diese häufig die Konvention val, err := doSomething() verwenden, um die gleiche Semantik wie der oben definierte Ergebnistyp anzugeben.
Außerdem ist die Erstellung dieses Ergebnisses umständlich: Result[int, string]{value: 1, err: ""}. Anstatt den Ergebnistyp bereitzustellen, möchten wir wahrscheinlich idiomatische Muster abgleichen, damit Go-Benutzer sich natürlich fühlen, wenn sie unsere generierten Bindungen nutzen.
Code kann so generiert werden, dass er sich natürlicher für die Sprache anfühlt, oder es kann eine direktere Zuordnung zu den Komponentenmodelltypen erfolgen. Keine der Optionen passt zu 100 % der Anwendungsfälle, daher liegt es an den Tool-Autoren, zu entscheiden, welche am sinnvollsten ist.
Für die Arcjet-Werkzeuge haben wir den idiomatischen Go-Ansatz für die Option<_> gewählt. und result<_, _> Typen, die jeweils auf val, ok := doSomething() und val, err := doSomething() abgebildet werden. Für Varianten erstellen wir eine Schnittstelle, die jede Variante implementieren muss, wie zum Beispiel:
type Result[V any, E any] struct { value V err E }
Dies schafft eine gute Balance zwischen Typsicherheit und unnötigem Umwickeln. Natürlich gibt es Situationen, in denen eine Umhüllung erforderlich ist, aber diese können als Randfälle behandelt werden.
Entwickler haben möglicherweise Probleme mit nicht idiomatischen Mustern, was zu ausführlichem und weniger wartbarem Code führt. Durch die Verwendung etablierter Konventionen fühlt sich der Code vertrauter an, die Implementierung erfordert jedoch etwas mehr Aufwand.
Wir haben uns für den idiomatischen Weg entschieden, um Reibungsverluste zu minimieren und es unserem Team einfacher zu machen, damit wir wissen, was uns erwartet, wenn wir uns in der Codebasis bewegen.
Eine der wichtigsten Entscheidungen, die Tool-Autoren treffen müssen, ist die Aufrufkonvention der Bindungen. Dazu gehört die Entscheidung, wie/wann Importe kompiliert werden, ob das Wasm-Modul während der Einrichtung oder Instanziierung kompiliert wird, und die Bereinigung.
In der Arcjet-Codebasis haben wir das Fabrik-/Instanzmuster ausgewählt, um die Leistung zu optimieren. Das Kompilieren eines WebAssembly-Moduls ist teuer, daher führen wir es einmal im NewBotFactory()-Konstruktor durch. Nachfolgende Instantiate()-Aufrufe sind dann schnell und kostengünstig und ermöglichen einen hohen Durchsatz bei Produktions-Workloads.
type BotConfig interface { isBotConfig() } func (AllowedBotConfig) isBotConfig() {} func (DeniedBotConfig) isBotConfig() {}
Verbraucher erstellen diese BotFactory einmal, indem sie NewBotFactory(ctx) aufrufen, und verwenden sie, um über die Instantiate-Methode mehrere Instanzen zu erstellen.
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 }
Die Instanziierung erfolgt sehr schnell, wenn das Modul bereits kompiliert wurde, wie wir es mit runtime.CompileModule() beim Erstellen der Factory tun.
Die BotInstance verfügt über Funktionen, die aus der Komponentenmodelldefinition exportiert wurden.
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 } }
Im Allgemeinen möchten wir eine BotInstance nach der Verwendung bereinigen, um sicherzustellen, dass kein Speicher verloren geht. Hierfür stellen wir die Close-Funktion zur Verfügung.
func (i *BotInstance) Detect( ctx context.Context, request string, options BotConfig, ) (BotResult, error) { // ... Lots of generated code for binding to Wazero }
Wenn Sie die gesamte BotFactory bereinigen möchten, kann diese auch geschlossen werden:
type Result[V any] struct { value V err error }
Wir können alle diese APIs zusammenfügen, um Funktionen in diesem WebAssembly-Modul aufzurufen:
type Result[V any, E any] struct { value V err E }
Dieses Muster der Fabrik- und Instanzkonstruktion erfordert mehr Code zur Verwendung, wurde jedoch ausgewählt, um in den Hot-Paths des Arcjet-Dienstes so viel Leistung wie möglich zu erzielen.
Durch die Vorabbelastung der Kompilierungskosten stellen wir sicher, dass in den Hot Paths des Arcjet-Dienstes – wo die Latenz am wichtigsten ist – die Anforderungsbearbeitung so effizient wie möglich ist. Dieser Kompromiss erhöht zwar die Komplexität des Initialisierungscodes, zahlt sich jedoch durch einen wesentlich geringeren Overhead pro Anfrage aus – siehe unsere Diskussion der Kompromisse.
Jedes Mal, wenn wir zwei oder mehr Sprachen integrieren müssen, ist dies voller Kompromisse, die eingegangen werden müssen – sei es mit nativem FFI oder dem Komponentenmodell.
In diesem Beitrag wurden einige der Herausforderungen besprochen, mit denen wir bei Arcjet konfrontiert waren, und die Gründe für unsere Entscheidungen. Wenn wir alle auf demselben Satz an Grundelementen wie dem Komponentenmodell und WIT aufbauen, können wir alle denselben Satz hochwertiger Grundelemente wie wit-bindgen oder wit-component nutzen und erstellen Sie Werkzeuge, die für jeden Anwendungsfall geeignet sind. Deshalb hilft die Arbeit an Standards allen.
Das WebAssembly-Komponentenmodell bietet eine leistungsstarke Abstraktion für die sprachübergreifende Integration, aber die Übersetzung seiner Typen in Sprachen wie Go bringt subtile Designherausforderungen mit sich. Durch die Auswahl idiomatischer Muster und die selektive Optimierung der Leistung – beispielsweise durch die Verwendung eines Fabrik-/Instanzmusters – können wir ein natürliches Entwicklererlebnis bieten und gleichzeitig die Effizienz aufrechterhalten.
Während sich die Tools rund um das Komponentenmodell weiterentwickeln, können wir uns auf verfeinerte Codegen-Ansätze freuen, die diese Integrationen weiter vereinfachen.
Das obige ist der detaillierte Inhalt vonDas Wasm-Komponentenmodell und das idiomatische Codegen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!