Interaktion mit Code

Emscripten bietet zahlreiche Methoden, um zwischen JavaScript und kompiliertem C oder C++ zu verbinden und zu interagieren

Dieser Artikel erklärt jede der oben aufgeführten Methoden und bietet Links zu detaillierteren Informationen.

Hinweis

Informationen darüber, wie kompilierter Code mit der Browserumgebung interagiert, finden Sie unter Emscripten-Laufzeitumgebung. Für dateisystembezogene Angelegenheiten siehe die Dateisystemübersicht.

Hinweis

Bevor Sie Ihren Code aufrufen können, muss die Laufzeitumgebung möglicherweise eine Speicherinitialisierungsdatei laden, Dateien vorladen oder andere asynchrone Operationen durchführen, abhängig von Optimierungs- und Build-Einstellungen. Siehe Wie erkenne ich, wann die Seite vollständig geladen ist und es sicher ist, kompilierte Funktionen aufzurufen? in den FAQ.

Aufrufen kompilierter C-Funktionen aus JavaScript mittels ccall/cwrap

Der einfachste Weg, kompilierte C-Funktionen aus JavaScript aufzurufen, ist die Verwendung von ccall() oder cwrap().

ccall() ruft eine kompilierte C-Funktion mit den angegebenen Parametern auf und gibt das Ergebnis zurück, während cwrap() eine kompilierte C-Funktion "umwickelt" und eine JavaScript-Funktion zurückgibt, die Sie normal aufrufen können. cwrap() ist daher nützlicher, wenn Sie planen, eine kompilierte Funktion mehrmals aufzurufen.

Betrachten Sie die unten gezeigte Datei test/hello_function.cpp. Die zu kompilierende Funktion int_sqrt() ist in extern "C" eingeschlossen, um C++-Namensmangling zu verhindern.

// Copyright 2012 The Emscripten Authors.  All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License.  Both these licenses can be
// found in the LICENSE file.

#include <math.h>

extern "C" {

int int_sqrt(int x) {
  return sqrt(x);
}

}

Um diesen Code zu kompilieren, führen Sie den folgenden Befehl im Emscripten-Home-Verzeichnis aus

emcc test/hello_function.cpp -o function.html -sEXPORTED_FUNCTIONS=_int_sqrt -sEXPORTED_RUNTIME_METHODS=ccall,cwrap

EXPORTED_FUNCTIONS weist den Compiler an, was vom kompilierten Code aus zugänglich sein soll (alles andere könnte entfernt werden, wenn es nicht verwendet wird), und EXPORTED_RUNTIME_METHODS weist den Compiler an, dass wir die Laufzeitfunktionen ccall und cwrap verwenden möchten (andernfalls werden sie nicht eingebunden).

Hinweis

EXPORTED_FUNCTIONS beeinflusst die Kompilierung zu JavaScript. Wenn Sie zuerst in eine Objektdatei kompilieren und dann die Objektdatei nach JavaScript kompilieren, benötigen Sie diese Option für den zweiten Befehl. Wenn Sie alles zusammen machen, wie in diesem Beispiel (Quellcode direkt nach JavaScript), dann funktioniert dies natürlich.

Nach dem Kompilieren können Sie diese Funktion mit cwrap() mit dem folgenden JavaScript aufrufen

int_sqrt = Module.cwrap('int_sqrt', 'number', ['number'])
int_sqrt(12)
int_sqrt(28)

Der erste Parameter ist der Name der zu umwickelnden Funktion, der zweite ist der Rückgabetyp der Funktion (oder ein JavaScript-Wert null, falls keiner vorhanden ist), und der dritte ist ein Array von Parametertypen (das weggelassen werden kann, wenn keine Parameter vorhanden sind). Die Typen sind "number" (für eine JavaScript-Zahl, die einem C-Integer, Float oder allgemeinen Zeiger entspricht), "string" (für einen JavaScript-String, der einem C char* entspricht, der einen String darstellt) oder "array" (für ein JavaScript-Array oder Typed-Array, das einem C-Array entspricht; bei Typed-Arrays muss es ein Uint8Array oder Int8Array sein).

Sie können dies selbst ausführen, indem Sie zuerst die generierte Seite function.html in einem Webbrowser öffnen (es wird beim Laden der Seite nichts passieren, da es keine main() gibt). Öffnen Sie eine JavaScript-Umgebung (Strg-Umschalt-K in Firefox, Strg-Umschalt-J in Chrome) und geben Sie die oben genannten Befehle als drei separate Befehle ein, wobei Sie nach jedem Enter drücken. Sie sollten die Ergebnisse 3 und 5 erhalten – die erwartete Ausgabe für diese Eingaben unter Verwendung der C++-Integer-Mathematik.

ccall() ist ähnlich, erhält aber einen weiteren Parameter mit den an die Funktion zu übergebenden Parametern

// Call C from JavaScript
var result = Module.ccall('int_sqrt', // name of C function
  'number', // return type
  ['number'], // argument types
  [28]); // arguments

// result is 5

Hinweis

Dieses Beispiel verdeutlicht einige weitere Punkte, die Sie bei der Verwendung von ccall() oder cwrap() beachten sollten

  • Diese Methoden können mit kompilierten C-Funktionen verwendet werden – Namens-gemangelte C++-Funktionen funktionieren nicht.

  • Wir empfehlen dringend, Funktionen, die von JavaScript aus aufgerufen werden sollen, zu exportieren

    • Der Export erfolgt zur Kompilierungszeit. Zum Beispiel: -sEXPORTED_FUNCTIONS=_main,_other_function exportiert main() und other_function().

    • Beachten Sie, dass Sie in der Liste EXPORTED_FUNCTIONS ein _ am Anfang der Funktionsnamen benötigen.

    • Beachten Sie, dass _main in dieser Liste erwähnt wird. Wenn Sie es dort nicht haben, wird der Compiler es als toten Code eliminieren. Die Liste der exportierten Funktionen ist die gesamte Liste, die am Leben erhalten wird (es sei denn, anderer Code wurde auf andere Weise am Leben erhalten).

    • Emscripten führt eine Dead-Code-Eliminierung durch, um die Codegröße zu minimieren – der Export stellt sicher, dass die benötigten Funktionen nicht entfernt werden.

    • Bei höheren Optimierungsstufen (-O2 und höher) wird Code minimiert, einschließlich Funktionsnamen. Das Exportieren von Funktionen ermöglicht es Ihnen, weiterhin über den ursprünglichen Namen auf diese Funktionen über das globale Module-Objekt zuzugreifen.

    • Wenn Sie eine JS-Bibliotheksfunktion (z.B. etwas aus einer Datei src/library*.js) exportieren möchten, müssen Sie diese zusätzlich zu EXPORTED_FUNCTIONS auch zu DEFAULT_LIBRARY_FUNCS_TO_INCLUDE hinzufügen, da letzteres erzwingt, dass die Methode tatsächlich in den Build aufgenommen wird.

  • Der Compiler entfernt nicht verwendeten Code, um die Codegröße zu verbessern. Wenn Sie ccall an einer Stelle verwenden, die er sieht, wie Code in einem --pre-js oder --post-js, wird es einfach funktionieren. Wenn Sie es an einer Stelle verwenden, die der Compiler nicht gesehen hat, wie einem anderen Skript-Tag im HTML oder in der JS-Konsole, wie wir es in diesem Tutorial getan haben, dann sollten Sie aufgrund von Optimierungen und Minifizierung ccall aus der Laufzeit exportieren, indem Sie EXPORTED_RUNTIME_METHODS verwenden, zum Beispiel mit -sEXPORTED_RUNTIME_METHODS=ccall,cwrap, und es auf Module aufrufen (welches alles Exportierte sicher enthält und nicht von Minifizierung oder Optimierungen beeinflusst wird).

Interaktion mit einer in C/C++ geschriebenen API von NodeJS

Angenommen, Sie haben eine C-Bibliothek, die einige Prozeduren bereitstellt

//api_example.c
#include <stdio.h>
#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
void sayHi() {
  printf("Hi!\n");
}

EMSCRIPTEN_KEEPALIVE
int daysInWeek() {
  return 7;
}

Kompilieren Sie die Bibliothek mit emcc

emcc api_example.c -o api_example.js -sMODULARIZE -sEXPORTED_RUNTIME_METHODS=ccall

Fordern Sie die Bibliothek an und rufen Sie ihre Prozeduren von Node auf

var factory = require('./api_example.js');

factory().then((instance) => {
  instance._sayHi(); // direct calling works
  instance.ccall("sayHi"); // using ccall etc. also work
  console.log(instance._daysInWeek()); // values can be returned, etc.
});

Die Option MODULARIZE bewirkt, dass emcc Code in einem modularen Format ausgibt, der einfach mit require() importiert und verwendet werden kann: require() des Moduls gibt eine Fabrikfunktion zurück, die den kompilierten Code instanziieren kann, und gibt ein Promise zurück, um uns mitzuteilen, wann es bereit ist, und gibt uns die Instanz des Moduls als Parameter.

(Beachten Sie, dass wir hier ccall verwenden, daher müssen wir es wie zuvor zu den exportierten Laufzeitmethoden hinzufügen.)

Kompilierten C/C++-Code „direkt“ aus JavaScript aufrufen

Funktionen in der ursprünglichen Quelle werden zu JavaScript-Funktionen, sodass Sie sie direkt aufrufen können, wenn Sie die Typumwandlungen selbst vornehmen – dies ist schneller als die Verwendung von ccall() oder cwrap(), aber etwas komplizierter.

Um die Methode direkt aufzurufen, müssen Sie den vollständigen Namen verwenden, wie er im generierten Code erscheint. Dieser ist derselbe wie der ursprüngliche C-Funktionsname, jedoch mit einem führenden _.

Hinweis

Wenn Sie ccall() oder cwrap() verwenden, müssen Sie Funktionsaufrufen kein _ voranstellen – verwenden Sie einfach den C-Namen.

Die Parameter, die Sie an Funktionen übergeben und von ihnen empfangen, müssen primitive Werte sein

  • Ganzzahlige und Gleitkommazahlen können unverändert übergeben werden.

  • Zeiger können ebenfalls unverändert übergeben werden, da sie im generierten Code einfach ganze Zahlen sind.

  • Der JavaScript-String someString kann mit ptr = stringToNewUTF8(someString) in einen char * konvertiert werden.

    Hinweis

    Die Konvertierung in einen Zeiger allokiert Speicher, der anschließend durch einen Aufruf von free(ptr) (_free auf JavaScript-Seite) freigegeben werden muss -

  • char *, das von C/C++ empfangen wird, kann mit UTF8ToString() in einen JavaScript-String konvertiert werden.

    Weitere Komfortfunktionen zum Konvertieren von Zeichenketten und Kodierungen finden Sie in preamble.js.

  • Andere Werte können über emscripten::val übergeben werden. Schauen Sie sich Beispiele zu as_handle und take_ownership Methoden an.

Aufrufen von JavaScript aus C/C++

Emscripten bietet zwei Hauptansätze zum Aufrufen von JavaScript aus C/C++: Ausführen des Skripts mit emscripten_run_script() oder Schreiben von „inline JavaScript“.

Der direkteste, aber etwas langsamere Weg ist die Verwendung von emscripten_run_script(). Dies führt den angegebenen JavaScript-Code effektiv aus C/C++ mittels eval() aus. Um beispielsweise die Browser-Funktion alert() mit dem Text 'hi' aufzurufen, würden Sie das folgende JavaScript aufrufen

emscripten_run_script("alert('hi')");

Hinweis

Die Funktion alert ist in Browsern vorhanden, aber nicht in Node oder anderen JavaScript-Shells. Eine allgemeinere Alternative ist der Aufruf von console.log.

Eine schnellere Methode, JavaScript von C aus aufzurufen, ist das Schreiben von „Inline JavaScript“ mit EM_JS() oder EM_ASM() (und verwandten Makros).

EM_JS wird verwendet, um JavaScript-Funktionen innerhalb einer C-Datei zu deklarieren. Das „alert“-Beispiel könnte mit EM_JS wie folgt geschrieben werden

#include <emscripten.h>

EM_JS(void, call_alert, (), {
  alert('hello world!');
  throw 'all done';
});

int main() {
  call_alert();
  return 0;
}

Die Implementierung von EM_JS ist im Wesentlichen eine Kurzform für die Implementierung einer JavaScript-Bibliothek.

EM_ASM wird ähnlich wie Inline-Assembler-Code verwendet. Das "alert"-Beispiel könnte mit Inline-JavaScript wie folgt geschrieben werden

#include <emscripten.h>

int main() {
  EM_ASM(
    alert('hello world!');
    throw 'all done';
  );
  return 0;
}

Beim Kompilieren und Ausführen wird Emscripten die beiden JavaScript-Zeilen so ausführen, als ob sie direkt im generierten Code erscheinen würden. Das Ergebnis wäre eine Warnung, gefolgt von einer Ausnahme. (Beachten Sie jedoch, dass Emscripten unter der Haube auch in diesem Fall einen Funktionsaufruf durchführt, was einen gewissen Overhead mit sich bringt.)

Sie können auch Werte von C in JavaScript innerhalb von EM_ASM senden, zum Beispiel

EM_ASM({
  console.log('I received: ' + $0);
}, 100);

Dies wird I received: 100 anzeigen.

Sie können auch Werte zurückerhalten, zum Beispiel wird Folgendes I received: 100 und dann 101 ausgeben

int x = EM_ASM_INT({
  console.log('I received: ' + $0);
  return $0 + 1;
}, 100);
printf("%d\n", x);

Weitere Details finden Sie in den emscripten.h docs.

Hinweis

  • Sie müssen angeben, ob der Rückgabewert ein int, double oder Zeigertyp ist, indem Sie das entsprechende Makro EM_ASM_INT, EM_ASM_DOUBLE oder EM_ASM_PTR verwenden. (EM_ASM_PTR ist dasselbe wie EM_ASM_INT, es sei denn, MEMORY64 wird verwendet, und wird daher hauptsächlich in Code benötigt, der mit MEMORY64 kompatibel sein soll).

  • Die Eingabewerte erscheinen als $0, $1 usw.

  • return wird verwendet, um den von JavaScript an C zurückgesendeten Wert bereitzustellen.

  • Beachten Sie, wie { und } hier verwendet werden, um den Code einzuschließen. Dies ist notwendig, um den Code von den später übergebenen Argumenten, die die Eingabewerte sind, zu unterscheiden (so funktionieren C-Makros).

  • Bei der Verwendung des Makros EM_ASM stellen Sie sicher, dass Sie nur einfache Anführungszeichen (') verwenden. Doppelte Anführungszeichen (") führen zu einem Syntaxfehler, der vom Compiler nicht erkannt wird und nur beim Betrachten einer JavaScript-Konsole während der Ausführung des fehlerhaften Codes angezeigt wird.

  • clang-format kann JavaScript-Konstrukte wie => in = > umwandeln. Um dies zu vermeiden, fügen Sie zu Ihrem .clang-format hinzu: WhitespaceSensitiveMacros: ['EM_ASM', 'EM_JS', 'EM_ASM_INT', 'EM_ASM_DOUBLE', 'EM_ASM_PTR', 'MAIN_THREAD_EM_ASM', 'MAIN_THREAD_EM_ASM_INT', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_EM_ASM_DOUBLE', 'MAIN_THREAD_ASYNC_EM_ASM']. Oder schalten Sie clang-format aus, indem Sie // clang-format off vor dem EM_ASM-Abschnitt und // clang-format on danach schreiben.

Eine C-API in JavaScript implementieren

Es ist möglich, eine C-API in JavaScript zu implementieren! Dies ist der Ansatz, der in vielen Emscripten-Bibliotheken wie SDL1 und OpenGL verwendet wird.

Sie können es verwenden, um Ihre eigenen APIs zu schreiben, die Sie von C/C++ aus aufrufen können. Dazu definieren Sie die Schnittstelle und dekorieren sie mit extern, um die Methoden in der API als externe Symbole zu kennzeichnen. Dann implementieren Sie die Symbole in JavaScript, indem Sie einfach deren Definition zu library.js (standardmäßig) hinzufügen. Beim Kompilieren des C-Codes sucht der Compiler in den JavaScript-Bibliotheken nach relevanten externen Symbolen.

Standardmäßig wird die Implementierung zu library.js hinzugefügt (und hier finden Sie Teile von Emscriptens libc). Sie können die JavaScript-Implementierung in Ihre eigene Bibliotheksdatei legen und sie mit der emcc-Option --js-library hinzufügen. Siehe test_js_libraries in test/test_other.py für ein vollständiges, funktionierendes Beispiel, einschließlich der Syntax, die Sie in der JavaScript-Bibliotheksdatei verwenden sollten.

Als einfaches Beispiel betrachten Sie den Fall, dass Sie C-Code wie diesen haben

extern void my_js(void);

int main() {
  my_js();
  return 1;
}

Hinweis

Bei der Verwendung von C++ sollten Sie extern void my_js(); in einen extern "C" {}-Block einschließen, um C++-Namensmangling zu verhindern

extern "C" {
  extern void my_js();
}

Dann können Sie my_js in JavaScript implementieren, indem Sie die Implementierung einfach zu library.js (oder Ihrer eigenen Datei) hinzufügen. Wie unsere anderen Beispiele zum Aufrufen von JavaScript aus C, erstellt das folgende Beispiel einfach ein Dialogfeld mit einer einfachen alert()-Funktion.

my_js: function() {
  alert('hi');
},

Wenn Sie es zu Ihrer eigenen Datei hinzufügen, sollten Sie etwas wie folgt schreiben

addToLibrary({
  my_js: function() {
    alert('hi');
  },
});

addToLibrary kopiert die Eigenschaften des Eingabeobjekts in LibraryManager.library (das globale Objekt, in dem sich der gesamte JavaScript-Bibliothekscode befindet). In diesem Fall fügt es eine Funktion namens my_js zu diesem Objekt hinzu.

JavaScript-Einschränkungen in Bibliotheksdateien

Wenn Sie mit JavaScript nicht vertraut sind, z.B. wenn Sie ein C/C++-Programmierer sind und nur Emscripten verwenden, dann werden die folgenden Probleme wahrscheinlich nicht auftreten, aber wenn Sie ein erfahrener JavaScript-Programmierer sind, müssen Sie sich bewusst sein, dass einige gängige JavaScript-Praktiken in Emscripten-Bibliotheksdateien nicht in bestimmten Weisen verwendet werden können.

Um Speicherplatz zu sparen, bindet Emscripten standardmäßig nur Bibliotheks-Eigenschaften ein, auf die von C/C++ verwiesen wird. Dies geschieht, indem für jede verwendete Eigenschaft in den verknüpften JavaScript-Bibliotheken toString aufgerufen wird. Das bedeutet, dass Sie beispielsweise keine Closure direkt verwenden können, da toString damit nicht kompatibel ist – genau wie bei der Verwendung eines Strings zum Erstellen eines Web Workers, wo Sie ebenfalls keine Closure übergeben können. (Beachten Sie, dass diese Einschränkung nur für die Werte der Schlüssel des Objekts gilt, das an addToLibrary in der JS-Bibliothek übergeben wird, d.h. die obersten Schlüssel-Wert-Paare sind speziell. Interner Code innerhalb einer Funktion kann natürlich beliebiges JS enthalten).

Um diese Einschränkung von JS-Bibliotheken zu vermeiden, können Sie Code in eine andere Datei legen, indem Sie die Optionen --pre-js oder --post-js verwenden, die beliebiges normales JS zulassen und mit dem Rest der Ausgabe eingeschlossen und optimiert werden. Dies ist der empfohlene Ansatz für die meisten Fälle. Eine weitere Option ist ein weiteres <script>-Tag.

Alternativ können Sie, wenn Sie eine JS-Bibliotheksdatei bevorzugen, eine Funktion sich selbst ersetzen lassen und sie während der Initialisierung aufrufen.

addToLibrary({

  // Solution for bind or referencing other functions directly
  good_02__postset: '_good_02();',
  good_02: function() {
    _good_02 = document.querySelector.bind(document);
  },

  // Solution for closures
  good_03__postset: '_good_03();',
  good_03: function() {
    var callCount = 0;
    _good_03 = function() {
      console.log("times called: ", ++callCount);
    };
  },

  // Solution for curry/transform
  good_05__postset: '_good_05();',
  good_05: function() {
    _good_05 = curry(scrollTo, 0);
 },

});

Ein __postset ist ein String, den der Compiler direkt in die Ausgabedatei schreibt. Für das obige Beispiel wird dieser Code ausgegeben.

 function _good_02() {
   _good_o2 = document.querySelector.bind(document);
 }

 function _good_03() {
   var callCount = 0;
   _good_03 = function() {
     console.log("times called: ", ++callCount);
   };
 }

 function _good_05() {
   _good_05 = curry(scrollTo, 0);
};

// Call each function once so it will replace itself
_good_02();
_good_03();
_good_05();

Sie können den größten Teil Ihres Codes auch in die xxx__postset-Strings einfügen. Im Beispiel deklariert jede Methode eine Abhängigkeit von $method_support und sind ansonsten Dummy-Funktionen. $method_support selbst hat eine entsprechende __postset-Eigenschaft mit dem gesamten Code, um die verschiedenen Methoden auf die Funktionen zu setzen, die wir tatsächlich wollen.

addToLibrary({
  $method_support: {},
  $method_support__postset: [
    '(function() {                                  ',
    '  var SomeLib = function() {                   ',
    '    this.callCount = 0;                        ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.getCallCount = function() {',
    '    return this.callCount;                     ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.process = function() {     ',
    '    ++this.callCount;                          ',
    '  };                                           ',
    '                                               ',
    '  SomeLib.prototype.reset = function() {       ',
    '    this.callCount = 0;                        ',
    '  };                                           ',
    '                                               ',
    '  var inst = new SomeLib();                    ',
    '  _method_01 = inst.getCallCount.bind(inst);   ',
    '  _method_02 = inst.process.bind(inst);        ',
    '  _method_03 = inst.reset.bind(inst);          ',
    '}());                                          ',
  ].join('\n'),
  method_01: function() {},
  method_01__deps: ['$method_support'],
  method_02: function() {},
  method_01__deps: ['$method_support'],
  method_03: function() {},
  method_01__deps: ['$method_support'],
 });

Hinweis: Wenn Sie Node 4.1 oder neuer verwenden, können Sie mehrzeilige Strings verwenden. Diese werden nur zur Kompilierungszeit und nicht zur Laufzeit verwendet, sodass die Ausgabe weiterhin in ES5-basierten Umgebungen ausgeführt wird.

Eine weitere Möglichkeit besteht darin, den größten Teil Ihres Codes in ein Objekt zu legen, nicht in eine Funktion,

addToLibrary({
  $method_support__postset: 'method_support();',
  $method_support: function() {
    var SomeLib = function() {
      this.callCount = 0;
    };

    SomeLib.prototype.getCallCount = function() {
      return this.callCount;
    };

    SomeLib.prototype.process = function() {
      ++this.callCount;
    };

    SomeLib.prototype.reset = function() {
      this.callCount = 0;
    };

    var inst = new SomeLib();
    _method_01 = inst.getCallCount.bind(inst);
    _method_02 = inst.process.bind(inst);
    _method_03 = inst.reset.bind(inst);
  },
  method_01: function() {},
  method_01__deps: ['$method_support'],
  method_02: function() {},
  method_01__deps: ['$method_support'],
  method_03: function() {},
  method_01__deps: ['$method_support'],
 });

Weitere Beispiele finden Sie in den Dateien library_*.js.

Hinweis

  • JavaScript-Bibliotheken können Abhängigkeiten (__deps) deklarieren, diese sind jedoch nur für andere JavaScript-Bibliotheken. Siehe Beispiele in /src mit dem Namensformat library_*.js

  • Sie können Abhängigkeiten für alle Ihre Methoden hinzufügen, indem Sie autoAddDeps(myLibrary, name) verwenden, wobei myLibrary das Objekt mit all Ihren Methoden ist und name die Sache ist, von der sie alle abhängen. Dies ist nützlich, wenn alle implementierten Methoden ein JavaScript-Singleton verwenden, das Hilfsmethoden enthält. Siehe library_webgl.js für ein Beispiel.

  • Die an addToLibrary übergebenen Schlüssel generieren Funktionen, denen ein _ vorangestellt wird. Mit anderen Worten, my_func: function() {}, wird zu function _my_func() {}, da alle C-Methoden in Emscripten ein _-Präfix haben. Schlüssel, die mit $ beginnen, haben das $ entfernt und kein Unterstrich hinzugefügt.

Aufrufen von JavaScript-Funktionen als Funktionszeiger von C

Sie können addFunction verwenden, um einen Integer-Wert zurückzugeben, der einen Funktionszeiger darstellt. Wenn dieser Integer dann an C-Code übergeben wird, kann dieser Wert als Funktionszeiger aufgerufen werden, und die JavaScript-Funktion, die Sie an addFunction gesendet haben, wird aufgerufen.

Siehe test_add_function in test/test_core.py für ein Beispiel.

Sie sollten mit -sALLOW_TABLE_GROWTH bauen, um das Hinzufügen neuer Funktionen zur Tabelle zu ermöglichen. Andernfalls hat die Tabelle standardmäßig eine feste Größe.

Wenn Sie addFunction mit einer JavaScript-Funktion verwenden, müssen Sie ein zusätzliches zweites Argument angeben, einen Wasm-Funktionssignaturstring, der unten erklärt wird. Siehe test/interop/test_add_function_post.js für ein Beispiel.

Funktionssignaturen

Das LLVM Wasm-Backend erfordert einen Wasm-Funktionssignaturstring bei der Verwendung von addFunction und in JavaScript-Bibliotheken. Jedes Zeichen innerhalb eines Signaturstrings repräsentiert einen Typ. Das erste Zeichen repräsentiert den Rückgabetyp einer Funktion, und die restlichen Zeichen sind für Parametertypen.

  • 'v': void-Typ

  • 'i': 32-Bit-Integer-Typ

  • 'j': 64-Bit-Integer-Typ (siehe Hinweis unten)

  • 'f': 32-Bit-Float-Typ

  • 'd': 64-Bit-Float-Typ

  • 'p': 32-Bit- oder 64-Bit-Zeiger (MEMORY64)

Wenn Sie beispielsweise eine Funktion hinzufügen, die eine Ganzzahl annimmt und nichts zurückgibt, ist die Signatur 'vi'.

Wenn 'j' verwendet wird, gibt es mehrere Möglichkeiten, wie der Parameterwert an JavaScript übergeben wird. Standardmäßig wird der Wert entweder als einzelne BigInt oder als Paar von JavaScript-Zahlen (double) übergeben, je nachdem, ob die Einstellung WASM_BIGINT aktiviert ist. Wenn Sie nur 53 Bit Präzision benötigen, können Sie zusätzlich den Dekorator __i53abi hinzufügen, der die oberen Bits ignoriert und der Wert als einzelne JavaScript-Zahl (double) empfangen wird. Er kann nicht mit addFunction verwendet werden. Hier ist ein Beispiel für eine Bibliotheksfunktion, die die Größe einer Datei mit einem 64-Bit-Wert als 53-Bit-Wert (double) festlegt und einen ganzzahligen Fehlercode zurückgibt

extern "C" int _set_file_size(int handle, uint64_t size);
_set_file_size__i53abi: true,  // Handle 64-bit
_set_file_size__sig: 'iij',    // Function signature
_set_file_size: function(handle, size) { ... return error; }

Die Verwendung von -sWASM_BIGINT beim Verlinken ist eine alternative Methode zur Handhabung von 64-Bit-Typen in Bibliotheken. `Number()` kann auf JavaScript-Seite erforderlich sein, um es in einen verwendbaren Wert zu konvertieren. Siehe Einstellungsreferenz.

Speicherzugriff von JavaScript aus

Sie können auf den Speicher zugreifen, indem Sie getValue(ptr, type) und setValue(ptr, value, type) verwenden. Das erste Argument ist ein Zeiger (eine Zahl, die eine Speicheradresse darstellt). type muss ein LLVM IR-Typ sein, einer von i8, i16, i32, i64, float, double oder ein Zeigertyp wie i8* (oder einfach *).

Es gibt Beispiele für die Verwendung dieser Funktionen in den Tests — siehe test/core/test_utf.in und test/test_core.py.

Hinweis

Dies ist eine Operation auf niedrigerer Ebene als ccall() und cwrap() – wir müssen uns darum kümmern, welcher spezifische Typ (z. B. Integer) verwendet wird.

Sie können auch direkt auf den Speicher zugreifen, indem Sie die Arrays manipulieren, die den Speicher darstellen. Dies wird nicht empfohlen, es sei denn, Sie sind sicher, dass Sie wissen, was Sie tun, und benötigen die zusätzliche Geschwindigkeit gegenüber getValue() und setValue().

Ein Fall, in dem dies erforderlich sein könnte, ist, wenn Sie eine große Datenmenge aus JavaScript importieren möchten, die von kompiliertem Code verarbeitet werden soll. Zum Beispiel allokiert der folgende Code einen Puffer, kopiert einige Daten hinein, ruft eine C-Funktion zur Verarbeitung der Daten auf und gibt schließlich den Puffer frei.

var buf = Module._malloc(myTypedArray.length*myTypedArray.BYTES_PER_ELEMENT);
Module.HEAPU8.set(myTypedArray, buf);
Module.ccall('my_function', 'number', ['number'], [buf]);
Module._free(buf);

Hier ist my_function eine C-Funktion, die einen einzelnen Integer-Parameter (oder einen Zeiger, beides sind für uns nur 32-Bit-Integer) empfängt und einen Integer zurückgibt. Dies könnte so etwas wie int my_function(char *buf) sein.

Der umgekehrte Fall des Exports von allokiertem Speicher in JavaScript kann knifflig sein, wenn der Wasm-basierte Speicher durch Kompilieren mit -sALLOW_MEMORY_GROWTH wachsen darf. Eine Erhöhung der Speichergröße führt zu einem neuen Puffer, und bestehende Array-Ansichten werden im Wesentlichen ungültig, sodass Sie dies nicht einfach tun können

function func() {
  let someView = HEAPU8.subarray(x, y);
  compute(someView);

  // This may grow memory, which would invalidate all views.
  maybeGrow();

  // If we grew, this use of an invalidated view will fail. Failure in this
  // case will return undefined, the same as reading out of bounds from a
  // typed array. If the operation were someView.subarray(), however, then it
  // would throw an error.
  return someView[z];
}

Emscripten aktualisiert die kanonischen Ansichten wie HEAPU8 für Sie, die Sie verwenden können, um Ihre eigenen Ansichten zu aktualisieren

function func() {
  let someView = HEAPU8.subarray(x, y);
  compute(someView);

  // This may grow memory, which would invalidate all views.
  maybeGrow();

  // Create a new, fresh view after the possible growth.
  someView = HEAPU8.subarray(x, y);
  return someView[z];
}

Eine weitere Möglichkeit, solche Probleme zu vermeiden, ist das Kopieren der Daten, wenn dies sinnvoll ist.

Ausführungsverhalten beeinflussen

Module ist ein globales JavaScript-Objekt mit Attributen, die der von Emscripten generierte Code an verschiedenen Punkten seiner Ausführung aufruft.

Entwickler stellen eine Implementierung von Module bereit, um zu steuern, wie Benachrichtigungen von Emscripten angezeigt werden, welche Dateien vor dem Ausführen der Hauptschleife geladen werden und eine Reihe anderer Verhaltensweisen. Weitere Informationen finden Sie unter Module-Objekt.

Umgebungsvariablen

Manchmal muss kompilierter Code auf Umgebungsvariablen zugreifen (zum Beispiel in C durch Aufrufen der Funktion getenv()). Der von Emscripten generierte JavaScript-Code kann nicht direkt auf die Umgebungsvariablen des Computers zugreifen, daher wird eine "virtualisierte" Umgebung bereitgestellt.

Das JavaScript-Objekt ENV enthält die virtualisierten Umgebungsvariablen, und durch dessen Modifikation können Sie Variablen an Ihren kompilierten Code übergeben. Es muss darauf geachtet werden, dass die Variable ENV von Emscripten initialisiert wurde, bevor sie geändert wird – die Verwendung von Module.preRun ist hierfür eine bequeme Methode.

Um beispielsweise eine Umgebungsvariable MY_FILE_ROOT auf "/usr/lib/test/" zu setzen, könnten Sie das folgende JavaScript zu Ihrem Module Setup-Code hinzufügen

Module.preRun = () => {ENV.MY_FILE_ROOT = "/usr/lib/test"};

Beachten Sie, dass Emscripten Standardwerte für einige Umgebungsvariablen (z.B. LANG) festlegt, nachdem Sie ENV konfiguriert haben, falls Sie keine eigenen Werte festgelegt haben. Wenn Sie möchten, dass solche Variablen nicht festgelegt bleiben, können Sie deren Wert explizit auf undefined setzen. Zum Beispiel

Module.preRun = () => {ENV.LANG = undefined};

C++ und JavaScript binden — WebIDL Binder und Embind

Die JavaScript-Methoden zum Aufrufen kompilierter C-Funktionen sind effizient, können aber nicht mit namens-gemangelten C++-Funktionen verwendet werden.

WebIDL Binder und Embind erstellen Bindungen zwischen C++ und JavaScript, wodurch C++-Code-Entitäten auf natürliche Weise von JavaScript aus verwendet werden können. Embind unterstützt zusätzlich den Aufruf von JavaScript-Code aus C++.

Embind kann nahezu jeden C++-Code binden, einschließlich komplexer C++-Konstrukte (z.B. shared_ptr und unique_ptr). Der WebIDL Binder unterstützt C++-Typen, die in WebIDL ausgedrückt werden können. Obwohl diese Teilmenge kleiner ist als die von Embind unterstützte, reicht sie für die meisten Anwendungsfälle mehr als aus – Beispiele für Projekte, die mit dem Binder portiert wurden, sind die Physik-Engines Box2D und Bullet.

Beide Tools ermöglichen die Nutzung von abgebildeten Elementen aus JavaScript auf ähnliche Weise. Sie operieren jedoch auf verschiedenen Ebenen und verwenden sehr unterschiedliche Ansätze zur Definition der Bindung

  • Embind deklariert Bindungen innerhalb der C/C++-Datei.

  • WebIDL-Binder deklariert die Bindung in einer separaten Datei. Diese wird durch das Binder-Tool geführt, um "Klebecode" zu erstellen, der dann mit dem Projekt kompiliert wird.

Hinweis

Es gibt keine starken Beweise dafür, dass ein Werkzeug hinsichtlich der Leistung „besser“ ist als das andere (es gibt keine vergleichenden Benchmarks), und beide wurden in einer Reihe von Projekten erfolgreich eingesetzt. Die Wahl eines Werkzeugs gegenüber dem anderen hängt in der Regel davon ab, welches am besten zum Projekt und dessen Build-System passt.

C/C++ und JavaScript binden - Node-API

Emnapi ist eine inoffizielle Node-API-Implementierung, die unter Emscripten verwendet werden kann. Wenn Sie ein bestehendes Node-API-Addon nach WebAssembly portieren oder denselben Bindungscode sowohl für Node.js native Addons als auch für WebAssembly kompilieren möchten, können Sie es ausprobieren. Weitere Details finden Sie in der Emnapi-Dokumentation.