Outils pour utilisateurs

Outils du site


rust:lib-c

Rust : intégrer du code C

Généralités

Intégrer du code C au sein de Rust est possible et n'offre pas de difficultés particulières si l'on respecte “l'état d'esprit” de Rust sur la sûreté mémoire.

Il n'y a pas toujours de “liaison directe” entre les valeurs gérées par Rust et celles en C - l'exemple type est celui des chaînes de caractères. Cependant là encore, Rust fournit les outils nécessaires pour résoudre la plupart des cas.

Voyons ici le cas d'une bibliothèque dynamique.

Mise en œuvre

Intérêts et résumé

Pourquoi intégrer des bibliothèques ?

  1. profiter de modules ou fonctions déjà écrites ;
  2. intégration dans un système ou une organisation existant(e) ;
  3. accès à la définition de structures ou énumération.

Comme l'indique le site Ubuntu.com cité plus haut : “Les bibliothèques partagées sont du code compilé destiné à être partagé entre plusieurs différents programmes. Ils sont distribués en tant que fichiers .so dans /usr/lib”.

Ici quatre étapes sont à distinguer :

  1. création d'un projet Rust avec Cargo ;
  2. création d'une bibliothèque en C et d'un code d'intégration Rust ;
  3. compilation du code C en .so (bibliothèque) et du projet Rust ;
  4. liaison et exécution avec la bibliothèque.

Précision : il n'y a pas de fichier “​d'​entêtes”​ en Rust. La déclaration se fait directement au sein du code source, tant pour les fonctions que les énumérations ou les structures.

Exemples

Première étape - mise en place du projet

Lancer la création d'un squelette de projet Rust via Cargo :

cargo new rust_c

Notre projet ressemble pour l'instant à ceci…

~/ rust_c/		---> origine du projet 
  /src/ 
    /main.rs 		---> source du projet Rust 
  /target/		---> dossier de destination de la compilation Rust
  /Cargo.lock
  /Cargo.toml		---> fichier de configuration Cargo 

… et finira par l'arborescence suivante :

~/ rust_c/ 
  /lib/			---> dossier des lib partagées 
    /libdoubler.so 	---> lib partagée 
  /src/ 
    /doubler.c 		---> source de la lib partagée 
    /main.rs 
  /target/ 
    ...
    /rust_c		---> exécutable (dans /release) 
  /Cargo.lock
  /Cargo.toml
  /build.rs 		---> script Rust de compilation 
  /lancement.sh 	---> lanceur de l'exécutable 

Deuxième étape - mise en place du projet

Nb : la bibliothèque partagée a un nom de technique : cdylib (“c dynamic library”).

Nous allons partager deux fonctions entre la bibliothèque et le main du programme : une fonction en passage par référence (triplet, nous verrons plus exactement ce que c'est), une fonction en passage par valeur (doubler).

main.rs
// liaison vers le nom de la cdylib (hors extension et préfixe) 
#[link(name="doubler")] 
// indication que la fonction n'est pas au sein du code Rust du projet 
extern { 
	// définition de la signature : passage par valeur 
	fn doubler(x: u32) -> u32; 
	// ici, passage par "référence" 
	fn tripler(x: *mut u32); 
}
 
fn main() { 
	let mut i: u32 = 1; 
	// bloc "unsafe" car le code ne provient pas de Rust (cf. notions d'emprunt et de possession) 
	unsafe { 
		println!("{:?} x 3 = ?", i ); 
		tripler( &mut i ); 
		println!("{} x 2 = {}", i, doubler( i ) ); 
    } 
} 

Le code de notre bibliothèque de fonction est pour le moins triviale :

doubler.c
void tripler(int *x) { 
	*x = *x * 3; 
} 
 
int doubler(int x) {
	return x * 2;
} 

Troisième étape - gestion de la compilation

Nous rajoutons un script de compilation à notre projet, que nous devons préalablement déclarer dans le fichier TOML :

Cargo.toml
[package]
name = "rust_c"
version = "0.1.0"
authors = ["julien <julien.garderon@gmail.com>"]
edition = "2018"
build="build.rs" # rajouter le script de compilation
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
[dependencies]

Notre script de compilation ne contient qu'une seule étape, détaillée plus loin :

build.rs
fn main() { 
	println!(r"cargo:rustc-link-search=./lib"); 
} 

Ne pas oublier de créer le dossier à la racine du projet lib et enfin de créer un fichier .sh pour superviser l'ensemble :

lancement.sh
#!/bin/bash
 
# compilation du C et envoi du résultat dans le dossier correspondant 
gcc -shared -o ./lib/libdoubler.so -fPIC ./src/doubler.c
 
# compilation sans exécution du projet 
cargo build --release  
 
# redéfinition pour le script en cours de LD_LIBRARY_PATH 
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:./lib"
 
# lancement de l'exécutable 
./target/release/rust_c

Rendre le fichier .sh exécutable et le lancer :

julien@JulienGPortable:~/Développement/rust_c$ chmod +x ./lancement.sh && ./lancement.sh 
   Compiling rust_c v0.1.0 (/home/julien/Développement/rust_c)
    Finished release [optimized] target(s) in 0.17s
1 x 3 = ?
3 x 2 = 6

La compilation se déroule en plus étape, avec d'abord la production de la bibliothèque partagée. Puis se déroule la compilation du projet Rust en tant que tel (ici limité à main.rs). Celle-ci se déroule avec le compilateur qui reçoit de notre script build.rs l'indication d'un chemin supplémentaire à vérifier pour trouver la correspondance vers la bibliothèque partagée :

	println!(r"cargo:rustc-link-search=native=./lib"); 

La documentation officielle nous offre d'ailleurs un exemple de compilation de fichiers C, sans passer par un script .sh :

build-alternate.rs
// Example custom build script.
fn main() {
	// Tell Cargo that if the given file changes, to rerun this build script.
	println!("cargo:rerun-if-changed=src/hello.c");
	// Use the `cc` crate to build a C file and statically link it.
	cc::Build::new()
		.file("src/hello.c")
		.compile("hello");
}

Contrairement à ce que l'on pourrait s'attendre, la macro println! renvoie donc un argument de contexte supplémentaire au compilateur Rust qui sera utilisé pour le projet, et non pas pour de l'affichage direct en console (sdtout redirigé).

Dans l'exemple donné, la compilation du fichier C est redemandée si celui-ci a changé.

Voir également la documentation spécifique sur les chemins pour cdylibs.

Dans quel cas utiliser un script .sh ou Cargo ?

Nb : build.rs n'a pas accès aux modules extérieurs (extern crate), même s'ils sont gérés par Cargo et déclarés dans le fichier TOML.

Un script .sh “libère” en quelque sorte des contraintes (faibles) imposées par Cargo, permettant d'assurer par exemple que certains dossiers existent, que des droits sont possibles, ou d'autres opérations telles que des opérations ou téléchargements supplémentaires.

Cependant si le projet a vocation à être compilé dans des environnements différents (Windows, Mac), Cargo est le meilleur usage pour la chaîne de compilation.

Quatrième étape - exécution ; quelques erreurs expliquées

Le programme est lancé par notre script .sh, qui a d'abord rajouté au contexte local d'exécution, un chemin supplémentaire vers le dossier contenant notre bibliothèque.

Sans cette étape, la compilation ne poserait pas de problème mais la machine ne trouverait pas la ressource nécessaire au fonctionnement du programme. Testons :

julien@JulienGPortable:~/Développement/rust_c$ ./lancement.sh 
   Compiling rust_c v0.1.0 (/home/julien/Développement/rust_c)
    Finished release [optimized] target(s) in 0.16s
./target/release/rust_c: error while loading shared libraries: libdoubler.so: cannot open shared object file: No such file or directory

Point notable et trompeur, le compilateur de Rust ne fait que partiellement son travail de vérification sur les déclarations de fonctions. Imaginons une erreur : la déclaration d'une fonction externe qui n'existe pas dans ma bibliothèque partagée :

#[link(name="doubler")] 
extern { 
    fn quadrupler( x: u32 ); 
} 

Tant que la fonction n'est pas appelée dans le code, une alerte est émise mais la compilation réussie, indiquant faussement que le problème n'est que son non-usage :

julien@JulienGPortable:~/Développement/rust_c$ ./lancement.sh 
   Compiling rust_c v0.1.0 (/home/julien/Développement/rust_c)
warning: foreign function is never used: `quadrupler`
  --> src/main.rs:14:5
   |
14 |     fn quadrupler( x: u32 ); 
   |     ^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default
 
    Finished release [optimized] target(s) in 0.15s
1 x 3 = ?
3 x 2 = 6

C'est évidemment faux et dès l'usage de la fonction au sein de Rust, la compilation échouera lamentablement :

julien@JulienGPortable:~/Développement/rust_c$ ./lancement.sh 
   Compiling rust_c v0.1.0 (/home/julien/Développement/rust_c)
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-L" "/home/julien/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" (... je passe le gros ...) x86_64-unknown-linux-gnu/lib/libcore-f4ba4e822614473c.rlib" "-Wl,--end-group" "/home/julien/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-dea9a4949e72b2ec.rlib" "-Wl,-Bdynamic" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil"
  = note: /usr/bin/ld : /home/julien/Développement/rust_c/target/release/deps/rust_c-83b3384bcc7d9b46.rust_c.471leunz-cgu.0.rcgu.o : dans la fonction « rust_c::main » :
          rust_c.471leunz-cgu.0:(.text._ZN6rust_c4main17he0649419767923d7E+0xd8) : référence indéfinie vers « quadrupler »
          collect2: error: ld returned 1 exit status
 
error: aborting due to previous error
 
error: could not compile `rust_c`.
 
To learn more, run the command again with --verbose.
(...) 

Les structures et énumérations

Aucune difficulté particulière, sinon des points de vigilance :

  1. les “structures opaques” du C sont gérées en Rust par des “structures unitaires” (des structures sans aucun champ déclaré : struct MaStruct;). Leur usage dans Rust est borné par beaucoup de blocs non-sûrs (car à chaque appel, possiblement un champ voulu n'est pas existant ou initialisé…). Il est préférable donc, de contourner autant que possible leur usage ;
  2. vous devez déclarer une structure ou une énumération comme représentée en C. Car Rust va optimiser leur organisation pour gagner quelques bits si possible ;
  3. certaines opérations de partages peuvent être complexes à résoudre et la transposition transparente C = Rust, est impossible.

Voyons le dernier point, en modifiant nos fichiers sources :

doubler.c
typedef struct rien {
    int valeur;
} rien;
 
void tripler_alt(rien *x) { 
	x->valeur = x->valeur * 3; 
} 
 
rien doubler_alt(rien x) {
    struct rien y; 
    y.valeur = x.valeur * 2; 
    return y; 
} 
main.rs
// afficher la variable en débug 
#[derive(Debug)] 
// oblige la représentation machine au format "C ANSI" 
#[repr(C)] 
// son nom importe peu, et peut être différent du C 
struct QuelqueChose { 
	i: u32 
} 
 
// liaison vers le nom de la cdylib (hors extension et préfixe) 
#[link(name="doubler")] 
// indication que la fonction n'est pas au sein du code Rust du projet 
extern { 
	// définition de la signature : passage par valeur 
    fn doubler_alt( x: QuelqueChose ) -> QuelqueChose; 
    // ici, passage par "référence" 
    fn tripler_alt( x: *mut QuelqueChose ); 
} 
 
// une structure unitaire commentée, tout à fait inutile, mais c'est pour l'exemple
// struct Bidule; 
 
fn main() { 
	let mut i: QuelqueChose = QuelqueChose { 
		i: 1 
	}; 
	// bloc "unsafe" car le code ne provient pas de Rust (cf. notion d'emprunt et de possession) 
    unsafe { 
    	println!("{:?} x 3 = ?", i ); 
    	tripler_alt( &mut i ); 
    	// le problème se situe au niveau de la macro 'println!' 
    	let y = QuelqueChose { i : i.i }; 
    	println!("{:?} x 2 = {:?}", y, doubler_alt( i ) ); 
    } 
} 

Tout se passe bien…

julien@JulienGPortable:~/Développement/rust_c$ ./lancement.sh
   Compiling rust_c v0.1.0 (/home/julien/Développement/rust_c)
    Finished release [optimized] target(s) in 0.17s
QuelqueChose { i: 1 } x 3 = ?
QuelqueChose { i: 3 } x 2 = QuelqueChose { i: 6 }

… car nous avons contourné le problème. Testons la fonction main suivante :

fn main() { 
	let mut i: QuelqueChose = QuelqueChose { 
		i: 1 
	}; 
	// bloc "unsafe" car le code ne provient pas de Rust (cf. notion d'emprunt et de possession) 
    unsafe { 
    	println!("{:?} x 3 = ?", i ); 
    	tripler_alt( &mut i ); 
    	println!("{:?} x 2 = {:?}", i, doubler_alt( i ) ); 
    } 
} 

… l’œil averti aura repéré l'usage simultané de “i” en deux endroits, de manière mutable. La conséquence directe ne se fait pas attendre et dès la compilation :

error[E0505]: cannot move out of `i` because it is borrowed
  --> src/main.rs:35:50
   |
35 |         println!("{:?} x 2 = {:?}", i, doubler_alt( i ) ); 
   |         --------------------------------------------^-----
   |         |                           |               |
   |         |                           |               move out of `i` occurs here
   |         |                           borrow of `i` occurs here
   |         borrow later used here
   |
   = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
 
error: aborting due to previous error
 
For more information about this error, try `rustc --explain E0505`.
error: could not compile `rust_c`.
 
To learn more, run the command again with --verbose.

L'usage de bibliothèques provenant du C ne dispense donc nullement des règles d'emprunt et de partage. Le cas est ici trivial mais peut se retrouver à différent moment, lors que les imbrications C/Rust s'enchaînent.

Pour le C, ce n'est pas un problème et peut même être une méthode d'organisation du code. Rust ne l'autorisera pas : c'est probablement la principale limite (et seule ?) à l'usage de C “direct” en Rust.

Lorsque les fonctions utilisées doivent avoir le même objet mutable au même moment, en plusieurs endroits, soit le clonage/la création temporaire d'objet, soit une révision du fonctionnement du programme s'imposent.

Partage de références ou de pointeurs ?

En Rust, le code s'organise principalement pour le passage des arguments de fonction :

  1. soit “par valeur”,
  2. soit “par référence” mutable ou non-mutable.

Une référence peut être partagée en mode mutable mais exclusif (un seul accès en écriture) ; soit en mode non-mutable mais partageable (plusieurs accès simultanés en lecture).

Le fonctionnement est décrit ici : “Rust et Python associés grâce au C”.

Ressources

rust/lib-c.txt · Dernière modification: 2020/05/10 19:25 de julieng

Outils de la page