Rust: Fonctions Rust qui peuvent être appelées depuis C

Créé le 2 févr. 2012  ·  16Commentaires  ·  Source: rust-lang/rust

Nous avons maintenant beaucoup de scénarios où les personnes créant des liaisons veulent pouvoir fournir un rappel qu'une fonction C peut appeler. La solution actuelle consiste à écrire une fonction C native qui utilise des API internes non spécifiées pour renvoyer un message au code Rust. Idéalement, il s'agit d'écrire aucun code C.

Voici une solution minimale pour créer des fonctions dans Rust qui peuvent être appelées à partir du code C. L'essentiel est : 1) nous avons encore un autre type de déclaration de fonction, 2) cette fonction ne peut pas être appelée à partir du code Rust, 3) sa valeur peut être considérée comme un pointeur opaque et dangereux, 4) elle cuit dans la magie de commutation de pile et s'adapte du C ABI au Rust ABI.

Déclarations des fonctions C-to-Rust (crust):

crust fn callback(a: *whatever) {
}

Obtenir un pointeur non sécurisé vers une fonction C ABI :

let callbackptr: *u8 = callback;

Nous pourrions également définir un type spécifiquement à cette fin.

Implémentation du compilateur :

C'est surtout simple, mais le trans devient moche. En trans, nous devrons faire fondamentalement le contraire de ce que nous faisons pour les fonctions de mod natives :

  • Générer une fonction C ABI en utilisant la signature déclarée
  • Générer une fonction shim qui prend les arguments C dans une structure
  • La fonction C fourre les arguments dans une structure
  • La fonction C appelle upcall_call_shim_on_rust_stack avec la structure des arguments et l'adresse de la fonction shim
  • Générer une fonction Rust ABI en utilisant la signature déclarée
  • La fonction shim extrait les arguments de la structure et appelle la fonction Rust

Implémentation à l'exécution :

L'environnement d'exécution doit changer de plusieurs manières pour que cela se produise :

  • Un nouvel appel pour revenir à la pile Rust
  • Les tâches doivent maintenir une pile de contextes Rust et de contextes C
  • A besoin d'une stratégie pour faire face à l'échec après avoir réintégré la pile Rust
  • A besoin d'une stratégie pour gérer le rendement après avoir réintégré la pile de rouille

Échec:

Nous ne pouvons pas simplement lever une exception après être rentré dans la pile Rust car il n'y a aucune garantie que le code natif puisse être déroulé avec des exceptions C++. Le langage Go ignorera apparemment toutes les images natives dans ce scénario, laissant tout fuir en cours de route. Au lieu de cela, nous abandonnerons - si l'utilisateur veut éviter une défaillance catastrophique, il doit utiliser son rappel Rust pour envoyer un message et revenir immédiatement.

Rendement :

Sans modification de la façon dont nous gérons les piles C, nous ne pouvons pas permettre aux fonctions Rust de basculer en contexte vers le planificateur après avoir réintégré la pile Rust à partir du code C. Je vois deux solutions :

1) Le rendement est différent après être rentré dans la pile de rouille et bloque simplement. Les tâches qui veulent faire cela doivent s'assurer qu'elles ont leur propre planificateur (#1721).
2) Au lieu d'exécuter du code natif à l'aide de la pile du planificateur, les tâches extrairont les piles C d'un pool situé dans chaque planificateur. Chaque fois qu'une tâche réintègre la pile C, elle vérifiera si elle en a déjà une et la réutilisera, sinon elle en demandera une nouvelle au planificateur. Cela permettrait au code Rust de toujours produire normalement sans bloquer le planificateur.

Je préfère la deuxième option.

Voir aussi #1508

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

Commentaire le plus utile

Veuillez pardonner cette résurrection, mais ce problème est lié à un morceau de y-combinator et à quelques autres sites, et j'ai récemment eu un noob à ce sujet, donc je note que, par ce numéro et les changements ultérieurs, appeler Rust de C c'est simple :

#[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());
}

Tous les 16 commentaires

Désolé d'intervenir ici, mais j'aimerais souligner que l'appel de fonctions C doit être rapide, comme dans _blazing_ fast. Si je veux écrire un jeu en rouille en utilisant une bibliothèque C comme Allegro, SDL ou Opengl, c'est essentiel. Sinon, le jeu ralentira dans le code de rendu où il y a beaucoup d'appels C, ce qui est inacceptable. Le compilateur de langage Go par défaut avec cgo a de tels problèmes.

Je préférerais donc une solution rapide, même si elle peut restreindre ce que la fonction côté Rust peut faire.

De plus, ne serait-ce pas une idée d'utiliser "native fn" au lieu de "crust fn" ou cela a-t-il un autre sens prévu?

@beoran vous avez ceci en arrière. Nous parlons de C appelant Rust. L'appel de fonctions C depuis Rust est déjà assez rapide (cela pourrait être un peu plus rapide).

OK je vois. Y a-t-il un moyen pour que je puisse aider à accélérer l'appel de C à partir de la rouille ?

@beoran comme je l'ai dit, c'est un peu orthogonal à ce problème... mais la meilleure chose que vous puissiez faire est probablement de créer une référence montrant à quel point les performances sont inadéquates. :)

OK, je le ferai quand j'aurai assez avancé dans l'emballage d'Allegro pour comparer la surcharge de l'appel de rust Rust avec l'appel de C. Je laisserai ce problème seul pour l'instant et j'en ouvrirai un nouveau une fois que j'aurai la référence.

Pourrions-nous éviter l'une des copies des arguments en faisant écrire la fonction C directement dans la pile Rust, comme le font actuellement les appels Rust->C ?

J'espère que la copie finale des arguments de la structure shim et dans les arguments de la fonction rust sera éliminée par l'inline. Je ne sais pas si c'est à cela que vous faites référence par contre.

Je crois que nos appels C copient actuellement les arguments dans une structure sur la pile Rust, puis copient cette structure dans la pile C.

@pcwalton Rust->C n'écrit actuellement pas directement dans la pile C car cela est spécifique à i386. Je voulais éviter d'avoir à écrire du code spécifique à une convention d'appel particulière, même au prix de certaines performances, afin de faire fonctionner le 64 bits. (Je pense que de telles optimisations pourraient avoir du sens maintenant, d'autant plus que # 1402 souligne que LLVM ne gère pas vraiment complètement les conventions d'appel _de toute façon_)

Après avoir lu la fonction de commutation de pile, la structure arg n'est pas du tout copiée entre les piles, le pointeur vers la pile précédente est simplement transmis à la fonction qui s'exécute sur la nouvelle pile, ce qui est parfaitement logique.

La structure arg n'est pas copiée, mais la fonction shim en chargera les valeurs et les réinsérera dans la nouvelle pile. L'ancien code utilisé pour écrire littéralement les valeurs des arguments directement dans la pile cible. Cela avait du sens sur i386, mais sur x86_64, il est beaucoup plus complexe de déterminer quelles valeurs iront sur la pile, etc.

Après quelques tests, j'ai découvert qu'il sera très difficile pour les fonctions de la croûte de garantir qu'elles n'échoueront pas. Ce qui se passe actuellement, c'est que, lorsqu'une tâche de niveau supérieur (comme principale) échoue, chaque tâche est appelée à échouer, donc dès que le rappel essaie d'envoyer un message (ou lorsqu'il revient de l'envoi d'un message), il peut finir par échouer et provoquant l'abandon anormal de l'exécution.

Je suppose que nous pouvons modifier rust_task pour ignorer les demandes d'élimination une fois que les tâches sont réintégrées dans la pile de rouille. Pour les tâches implémentant des boucles d'événements, ils peuvent jeter un coup d'œil à un port de surveillance à la recherche d'un message indiquant que l'exécution échoue et comprendre comment se terminer normalement.

Ainsi, lorsqu'une tâche de niveau supérieur échoue, elle propage des erreurs à ses enfants ? Je suppose que je ne comprends pas notre modèle de propagation d'erreurs, je pensais qu'il partait des feuilles vers le haut. Il semble que le code exécuté sur des piles C devrait pouvoir s'exécuter dans une tâche non supervisée ou quelque chose comme ça.

Il agit essentiellement comme si 'main' était supervisé par le noyau, donc si main échoue, tout échoue.

J'appelle ça terminé. Il y a un petit nettoyage, et j'ai classé des bogues séparés pour les problèmes restants.

Veuillez pardonner cette résurrection, mais ce problème est lié à un morceau de y-combinator et à quelques autres sites, et j'ai récemment eu un noob à ce sujet, donc je note que, par ce numéro et les changements ultérieurs, appeler Rust de C c'est simple :

#[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());
}
Cette page vous a été utile?
0 / 5 - 0 notes