Rust: Rust-Funktionen, die von C aus aufgerufen werden können

Erstellt am 2. Feb. 2012  ·  16Kommentare  ·  Quelle: rust-lang/rust

Wir haben jetzt viele Szenarien, in denen Leute, die Bindungen erstellen, in der Lage sein möchten, einen Rückruf bereitzustellen, den eine C-Funktion aufrufen kann. Die aktuelle Lösung besteht darin, eine native C-Funktion zu schreiben, die einige nicht spezifizierte interne APIs verwendet, um eine Nachricht an Rust-Code zurückzusenden. Im Idealfall geht es darum, keinen C-Code zu schreiben.

Hier ist eine minimale Lösung zum Erstellen von Funktionen in Rust, die aus C-Code aufgerufen werden können. Das Wesentliche ist: 1) Wir haben noch eine andere Art von Funktionsdeklaration, 2) diese Funktion kann nicht aus Rust-Code aufgerufen werden, 3) ihr Wert kann als undurchsichtiger unsicherer Zeiger verwendet werden, 4) sie backt in der Stack-Switching-Magie und passt sich an vom C-ABI zum Rust-ABI.

Deklarationen von C-to-Rust (Crust)-Funktionen:

crust fn callback(a: *whatever) {
}

Einen unsicheren Zeiger auf eine C ABI-Funktion abrufen:

let callbackptr: *u8 = callback;

Wir könnten auch einen bestimmten Typ speziell für diesen Zweck definieren.

Compiler-Implementierung:

Es ist meistens einfach, aber Trans wird hässlich. In trans müssen wir im Grunde das Gegenteil von dem tun, was wir für native Mod-Funktionen tun:

  • Generieren Sie eine C ABI-Funktion mit der deklarierten Signatur
  • Generieren Sie eine Shim-Funktion, die die C-Argumente in einer Struktur übernimmt
  • Die C-Funktion stopft die Argumente in eine Struktur
  • Die C-Funktion ruft upcall_call_shim_on_rust_stack mit der Struktur von Argumenten und der Adresse der Shim-Funktion auf
  • Generieren Sie eine Rust ABI-Funktion mit der deklarierten Signatur
  • Die Shim-Funktion holt die Argumente aus der Struktur und ruft die Rust-Funktion auf

Laufzeitimplementierung:

Dazu muss sich die Laufzeit auf verschiedene Weise ändern:

  • Ein neuer Aufruf zum Wechsel zurück zum Rust-Stack
  • Aufgaben müssen einen Stapel von Rust-Kontexten und C-Kontexten verwalten
  • Benötigt eine Strategie, um mit Fehlern umzugehen, nachdem der Rust-Stack erneut betreten wurde
  • Benötigt eine Strategie, um mit dem Nachgeben umzugehen, nachdem der Roststapel wieder betreten wurde

Versagen:

Wir können nicht einfach eine Ausnahme auslösen, nachdem wir den Rust-Stack erneut aufgerufen haben, da es keine Garantie gibt, dass der native Code mit C++-Ausnahmen abgewickelt werden kann. Die Go-Sprache wird in diesem Szenario anscheinend einfach alle nativen Frames überspringen und dabei alles durchsickern lassen. Wir werden stattdessen abbrechen - wenn der Benutzer einen katastrophalen Fehler vermeiden möchte, sollte er seinen Rust-Callback verwenden, um eine Nachricht zu senden und sofort zurückzukehren.

Ergiebigkeit:

Ohne Änderungen an der Art und Weise, wie wir C-Stacks behandeln, können wir Rust-Funktionen nicht erlauben, den Kontext zum Scheduler zu wechseln, nachdem der Rust-Stack aus C-Code erneut eingegeben wurde. Ich sehe zwei Lösungen:

1) Der Ertrag ist nach dem erneuten Betreten des Rust-Stack anders und blockiert einfach. Tasks, die dies tun möchten, sollten sicherstellen, dass sie über einen eigenen Scheduler (#1721) verfügen.
2) Anstatt nativen Code mit dem Stack des Schedulers auszuführen, checken Tasks C-Stacks aus einem Pool aus, der sich in jedem Scheduler befindet. Jedes Mal, wenn eine Aufgabe wieder in den C-Stack eintritt, prüft sie, ob sie bereits einen hat und verwendet ihn wieder, andernfalls fordert sie einen neuen vom Scheduler an. Dies würde es Rust-Code ermöglichen, immer normal zu liefern, ohne den Scheduler zu binden.

Ich bevorzuge die zweite Möglichkeit.

Siehe auch #1508

A-debuginfo A-runtime A-typesystem E-easy

Hilfreichster Kommentar

Bitte verzeihen Sie diese Auferstehung, aber dieses Problem ist von einem Y-Kombinator-Stück und einigen anderen Seiten verlinkt, und ich hatte kürzlich einen Neuling, der danach gefragt hat, also stelle ich fest, dass ich gemäß dieser Ausgabe und späteren Änderungen Rust von C aus anrufe ist einfach :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}

Alle 16 Kommentare

Es tut mir leid, hier einzuspringen, aber ich möchte betonen, dass das Aufrufen von C-Funktionen schnell sein sollte, wie in _blazing_ fast. Wenn ich ein Spiel in Rust mit einer C-Bibliothek wie Allegro, SDL oder Opengl schreiben möchte, ist dies unerlässlich. Andernfalls wird das Spiel im Rendering-Code verlangsamt, wenn es viele C-Aufrufe gibt, was inakzeptabel ist. Der standardmäßige Go-Sprachcompiler mit cgo hat solche Probleme.

Daher würde ich eine schnelle Lösung bevorzugen, auch wenn sie möglicherweise die Möglichkeiten der Funktion auf der Rust-Seite einschränkt.

Wäre es nicht auch eine Idee, "native fn" anstelle von "crust fn" zu verwenden, oder hat das eine andere geplante Bedeutung?

@beoran Sie haben das rückwärts. Wir sprechen über C, das Rust anruft. Das Aufrufen von C-Funktionen aus Rust ist schon ziemlich schnell (könnte etwas schneller gemacht werden).

OK, ich verstehe. Gibt es eine Möglichkeit, wie ich das Anrufen von C aus Rost beschleunigen kann?

@beoran wie gesagt, es ist irgendwie orthogonal zu diesem Problem ... aber wahrscheinlich das Beste, was Sie tun können, ist, einen Benchmark zu

OK, ich werde das tun, wenn ich mit Allegro weit genug komme, um den Aufwand für den Aufruf von Rust Rust mit dem Aufrufen von C zu vergleichen. Ich lasse dieses Problem vorerst in Ruhe und eröffne ein neues, sobald ich es habe der Benchmark.

Könnten wir eine der Kopien der Argumente vermeiden, indem wir die C-Funktion direkt in den Rust-Stack schreiben lassen, wie es derzeit Rust->C-Aufrufe tun?

Ich hoffe, dass die endgültige Kopie der Argumente aus der Shim-Struktur und in die Argumente der Rostfunktion durch Inlining eliminiert wird. Ich bin mir aber nicht sicher, ob du das meinst.

Ich glaube, dass unsere C-Aufrufe derzeit die Argumente in eine Struktur auf dem Rust-Stack kopieren und dann diese Struktur in den C-Stack kopieren.

@pcwalton Rust->C schreibt derzeit nicht direkt in den C-Stack, da dies spezifisch für i386 ist. Ich wollte vermeiden, Code schreiben zu müssen, der für eine bestimmte Aufrufkonvention spezifisch ist, selbst um den Preis einer gewissen Leistung, um 64-Bit zum Laufen zu bringen. (Ich denke jedoch, dass solche Optimierungen jetzt sinnvoll sein könnten---insbesondere da #1402 darauf hinweist, dass LLVM die Aufrufkonventionen _jedenfalls_ nicht wirklich vollständig handhabt)

Nach dem Lesen der Stack-Switching-Funktion wird die arg-Struktur überhaupt nicht über die Stacks kopiert, der Zeiger auf den vorherigen Stack wird nur an die Funktion übergeben, die auf dem neuen Stack ausgeführt wird, was durchaus Sinn macht.

Die arg-Struktur wird nicht kopiert, aber die Shim-Funktion lädt Werte daraus und schiebt sie erneut auf den neuen Stack. Der alte Code, der verwendet wurde, um die Argumentwerte buchstäblich direkt in den Zielstapel zu schreiben. Dies war auf i386 sinnvoll, aber auf x86_64 ist es viel komplexer herauszufinden, welche Werte auf den Stack usw.

Nach einigen Tests habe ich festgestellt, dass es für Crust-Funktionen sehr schwierig sein wird, sicherzustellen, dass sie nicht versagen. Was derzeit passiert, ist, dass, wenn eine Aufgabe der obersten Ebene (wie main) fehlschlägt, jede Aufgabe fehlschlagen soll. Sobald der Rückruf also versucht, eine Nachricht zu senden (oder wenn er vom Senden einer Nachricht zurückkehrt), kann er fehlschlagen und Veranlassen, dass die Laufzeit abnormal abgebrochen wird.

Ich denke, wir können rust_task so ändern, dass Kill-Anfragen ignoriert werden, sobald Aufgaben wieder in den Rost-Stack aufgenommen wurden. Für Aufgaben, die Ereignisschleifen implementieren, können sie einen Blick auf einen Monitor-Port werfen, nach einer Meldung suchen, die darauf hinweist, dass die Laufzeit fehlschlägt, und herausfinden, wie sie ordnungsgemäß beendet werden kann.

Wenn also eine Aufgabe der obersten Ebene fehlschlägt, gibt sie Fehler an ihre untergeordneten Elemente weiter? Ich glaube, ich verstehe unser Fehlerausbreitungsmodell nicht, ich dachte, es ging von Anfang an aufwärts. Es scheint, als ob Code, der auf C-Stacks läuft, in einer unüberwachten Aufgabe oder ähnlichem ausgeführt werden sollte.

Es verhält sich im Grunde so, als ob 'main' vom Kernel überwacht wird. Wenn also main fehlschlägt, schlägt alles fehl.

Ich nenne das erledigt. Es gibt eine kleine Bereinigung, und ich habe separate Fehler für verbleibende Probleme eingereicht.

Bitte verzeihen Sie diese Auferstehung, aber dieses Problem ist von einem Y-Kombinator-Stück und einigen anderen Seiten verlinkt, und ich hatte kürzlich einen Neuling, der danach gefragt hat, also stelle ich fest, dass ich gemäß dieser Ausgabe und späteren Änderungen Rust von C aus anrufe ist einfach :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

Robbepop picture Robbepop  ·  3Kommentare

modsec picture modsec  ·  3Kommentare

pedrohjordao picture pedrohjordao  ·  3Kommentare

dnsl48 picture dnsl48  ·  3Kommentare

behnam picture behnam  ·  3Kommentare