ABI-Stabilität
Einführung
Eine Application Binary Interface (ABI) ist eine Möglichkeit für Programme, Funktionen aufzurufen und Datenstrukturen aus anderen kompilierten Programmen zu verwenden. Sie ist die kompilierte Version einer Application Programming Interface (API). Mit anderen Worten, die Header-Dateien, die die Klassen, Funktionen, Datenstrukturen, Aufzählungen und Konstanten beschreiben, die es einer Anwendung ermöglichen, eine gewünschte Aufgabe auszuführen, entsprechen durch die Kompilierung einer Reihe von Adressen und erwarteten Parameterwerten sowie Speicherstrukturgrößen und -layouts, mit denen der Anbieter der ABI kompiliert wurde.
Die Anwendung, die die ABI verwendet, muss so kompiliert werden, dass die verfügbaren Adressen, erwarteten Parameterwerte und Speicherstrukturgrößen und -layouts mit denen übereinstimmen, mit denen der ABI-Anbieter kompiliert wurde. Dies wird normalerweise durch die Kompilierung gegen die Header erreicht, die vom ABI-Anbieter bereitgestellt werden.
Da der Anbieter der ABI und der Benutzer der ABI zu unterschiedlichen Zeiten mit verschiedenen Versionen des Compilers kompiliert werden können, liegt ein Teil der Verantwortung für die Gewährleistung der ABI-Kompatibilität beim Compiler. Verschiedene Versionen des Compilers, die möglicherweise von verschiedenen Anbietern bereitgestellt werden, müssen alle die gleiche ABI aus einer Header-Datei mit einem bestimmten Inhalt erzeugen und müssen Code für die Anwendung erzeugen, die die ABI verwendet und die API, die in einem bestimmten Header beschrieben ist, gemäß den Konventionen der ABI, die sich aus der Beschreibung im Header ergibt, aufruft. Moderne Compiler haben eine ziemlich gute Erfolgsbilanz bei der Vermeidung von ABI-Kompatibilitätsproblemen bei den von ihnen kompilierten Anwendungen.
Die verbleibende Verantwortung für die Gewährleistung der ABI-Kompatibilität liegt beim Team, das die Header-Dateien pflegt, die die API bereitstellen, die nach der Kompilierung zu der ABI führt, die stabil bleiben soll. Änderungen an den Header-Dateien können vorgenommen werden, aber die Art der Änderungen muss genau verfolgt werden, um sicherzustellen, dass sich die ABI bei der Kompilierung nicht so ändert, dass bestehende Benutzer der ABI mit der neuen Version inkompatibel werden.
ABI-Stabilität in Node.js
Node.js stellt Headerdateien bereit, die von mehreren unabhängigen Teams gepflegt werden. So werden beispielsweise Headerdateien wie node.h
und node_buffer.h
vom Node.js-Team gepflegt. v8.h
wird vom V8-Team gepflegt, das zwar eng mit dem Node.js-Team zusammenarbeitet, aber unabhängig ist und seinen eigenen Zeitplan und seine Prioritäten hat. Daher hat das Node.js-Team nur eine eingeschränkte Kontrolle über die Änderungen, die in den Headern des Projekts eingeführt werden. Aus diesem Grund hat das Node.js-Projekt semantische Versionierung eingeführt. Dies stellt sicher, dass die vom Projekt bereitgestellten APIs zu einer stabilen ABI für alle Neben- und Patchversionen von Node.js führen, die innerhalb einer Hauptversion veröffentlicht werden. In der Praxis bedeutet dies, dass sich das Node.js-Projekt verpflichtet hat sicherzustellen, dass ein Node.js-natives Addon, das gegen eine bestimmte Hauptversion von Node.js kompiliert wurde, erfolgreich geladen wird, wenn es von einer beliebigen Node.js-Neben- oder Patchversion innerhalb der Hauptversion geladen wird, gegen die es kompiliert wurde.
N-API
Es ist die Forderung entstanden, Node.js mit einer API auszustatten, die zu einer ABI führt, die über mehrere Hauptversionen von Node.js hinweg stabil bleibt. Die Motivation für die Erstellung einer solchen API ist wie folgt:
Die JavaScript-Sprache ist seit ihren Anfängen mit sich selbst kompatibel geblieben, während sich die ABI der Engine, die den JavaScript-Code ausführt, mit jeder Hauptversion von Node.js ändert. Dies bedeutet, dass Anwendungen, die aus Node.js-Paketen bestehen, die vollständig in JavaScript geschrieben sind, nicht neu kompiliert, neu installiert oder neu bereitgestellt werden müssen, wenn eine neue Hauptversion von Node.js in die Produktionsumgebung gelangt, in der solche Anwendungen ausgeführt werden. Wenn eine Anwendung jedoch von einem Paket abhängt, das ein natives Addon enthält, muss die Anwendung neu kompiliert, neu installiert und neu bereitgestellt werden, wenn eine neue Hauptversion von Node.js in die Produktionsumgebung eingeführt wird. Diese Diskrepanz zwischen Node.js-Paketen, die native Addons enthalten, und solchen, die vollständig in JavaScript geschrieben sind, hat den Wartungsaufwand von Produktionssystemen, die auf native Addons angewiesen sind, erhöht.
Andere Projekte haben damit begonnen, JavaScript-Schnittstellen zu produzieren, die im Wesentlichen alternative Implementierungen von Node.js darstellen. Da diese Projekte in der Regel auf einer anderen JavaScript-Engine als V8 aufbauen, nehmen ihre nativen Addons notwendigerweise eine andere Struktur an und verwenden eine andere API. Dennoch würde die Verwendung einer einzigen API für ein natives Addon über verschiedene Implementierungen der Node.js-JavaScript-API hinweg es diesen Projekten ermöglichen, das Ökosystem von JavaScript-Paketen zu nutzen, das sich um Node.js herum angesammelt hat.
Node.js kann in Zukunft eine andere JavaScript-Engine enthalten. Das bedeutet, dass extern alle Node.js-Schnittstellen gleich bleiben würden, die V8-Headerdatei aber fehlen würde. Ein solcher Schritt würde das Node.js-Ökosystem im Allgemeinen und das der nativen Addons im Besonderen stören, wenn nicht zuerst von Node.js eine API bereitgestellt wird, die JavaScript-Engine-agnostisch ist, und von nativen Addons übernommen wird.
Zu diesen Zwecken hat Node.js in Version 8.6.0 N-API eingeführt und es mit Node.js 8.12.0 als stabile Komponente des Projekts gekennzeichnet. Die API ist in den Headern node_api.h
und node_api_types.h
definiert und bietet eine Vorwärtskompatibilitätsgarantie, die die Hauptversionsgrenze von Node.js überschreitet. Die Garantie kann wie folgt formuliert werden:
Eine gegebene Version n von N-API ist in der Hauptversion von Node.js, in der sie veröffentlicht wurde, und in allen nachfolgenden Versionen von Node.js, einschließlich nachfolgender Hauptversionen, verfügbar.
Ein Autor eines nativen Addons kann die N-API-Vorwärtskompatibilitätsgarantie nutzen, indem er sicherstellt, dass das Addon nur APIs verwendet, die in node_api.h
definiert sind, und Datenstrukturen und Konstanten, die in node_api_types.h
definiert sind. Auf diese Weise erleichtert der Autor die Einführung seines Addons, indem er den Produktionsbenutzern signalisiert, dass der Wartungsaufwand für ihre Anwendung durch das Hinzufügen des nativen Addons zu ihrem Projekt nicht mehr steigt als durch das Hinzufügen eines Pakets, das rein in JavaScript geschrieben ist.
N-API wird versioniert, da von Zeit zu Zeit neue APIs hinzugefügt werden. Im Gegensatz zur semantischen Versionierung ist die N-API-Versionierung kumulativ. Das heißt, jede Version von N-API hat die gleiche Bedeutung wie eine Nebenversion im Semver-System, was bedeutet, dass alle Änderungen an N-API abwärtskompatibel sind. Darüber hinaus werden neue N-APIs unter einem experimentellen Flag hinzugefügt, um der Community die Möglichkeit zu geben, sie in einer Produktionsumgebung zu überprüfen. Der experimentelle Status bedeutet, dass zwar darauf geachtet wurde, dass die neue API in Zukunft nicht auf ABI-inkompatible Weise geändert werden muss, sie aber in der Produktion noch nicht ausreichend auf ihre Korrektheit und Nützlichkeit in der vorgesehenen Form geprüft wurde und daher möglicherweise ABI-inkompatible Änderungen erfährt, bevor sie endgültig in eine zukünftige Version von N-API aufgenommen wird. Das heißt, eine experimentelle N-API ist noch nicht durch die Vorwärtskompatibilitätsgarantie abgedeckt.