Pour réaliser notre projet nous devons pouvoir nous interfacer en Rust avec la bibliothèque C ZBoss. Le C étant beaucoup plus dangereux que le Rust, et différent, de nombreuses précautions sont à prendre. Cet article recense les outils et les bonnes pratiques à suivre pour interagir avec du code étranger dans notre projet.
Caisses intéressantes
cty
Fournit les types C de base (on ne peut pas utiliser libc
pour notre cible).
On a les correspondances suivantes :
C | cty | Rust (ARM) |
---|---|---|
char | c_char | i8 |
signed char | c_schar | i8 |
unsigned char | c_uchar | u8 |
short | c_short | i16 |
unsigned short | c_ushort | u16 |
int | c_int | i32 |
unsigned (int) | c_uint | u32 |
long | c_long | i32 |
unsigned long | c_ulong | u32 |
long long | c_longlong | i64 |
unsigned long long | c_ulonglong | u64 |
float | c_float | f32 |
double | c_double | f64 |
X | X | bool |
T * | X | *mut T |
const T * | X | *const T |
void * | *mut c_void | *mut c_void |
const void * | *const c_void | *const c_void |
size_t | size_t | usize |
ptrdiff_t | ptrdiff_t | isize |
Bien entendu les types de taille fixe de stdint
peuvent être utilisés :
C (stdint.h) | cty | Rust |
---|---|---|
int8_t | int8_t | i8 |
uint8_t | uint8_t | u8 |
int16_t | int16_t | i16 |
uint16_t | uint16_t | u16 |
uint32_t | uint32_t | u32 |
int32_t | int32_t | i32 |
int64_t | int64_t | i64 |
uint64_t | uint64_t | u64 |
cstr_core
Les chaînes de caractères en Rust ne sont pas construites de la même manière qu’en C : il n’y a pas de caractère nul à la fin, et les caractères nuls sont admis au sein de la chaîne.
cstr_core
fourni CStr
(et CString
si l’allocation est activée) comme chaîne se comportant comme en C.
rust-bindgen
A partir des entêtes C (éventuellement C++) d’une bibliothèque, génère les prototypes et les structures de données à l’interface adaptés à Rust.
Note : la caisse peut être utilisée pour la génération (build script, donc utilisé comme dépendance dans Cargo sous [build.dependencies]
), puis enlevée des dépendances pour alléger le projet, ou encore utilisée dans une caisse à part du projet dédiée aux bindings.
Attributs utiles
#[link(name = « ext_library »)]
Indique au compilateur que les fonctions d’un bloc extern
sont à lier à la bibliothèque donnée.
Exemple :
#[link(name = "git2")]
extern "C" {
pub fn git_libgit2_init() -> c_int;
pub fn git_libgit2_shutdown() -> c_int;
. . .
}
#[repr(C)]
Force l’agencement en mémoire d’une structure à suivre les règles du C (ordre des champs, espacement entre les champs, alignement)
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int,
}
#repr(align(n))
Force l’alignement sur n bytes, n doit être une puissance de 2.
#[no_mangle]
Empêche le mangling
par défaut du symbole, utile pour aider le linker à retrouver les symboles en multi-langage.
Techniques et bonnes pratiques
Unsafe
et contrat
Lorsqu’une fonction ou un trait sont déclarés comme unsafe
il est impératif de documenter le contrat d’utilisation, c’est-à-dire de préciser le cadre d’utilisation pour ne pas avoir de comportement indéfini.
Exemple dans la bibliothèque standard : la fonction Vec::get_unchecked
:
pub unsafe fn get_unchecked<I>(&self, index: I) -> &I::Output
where
I: SliceIndex<Self>,
{
// SAFETY: the caller must uphold most of the safety requirements for `get_unchecked`;
// the slice is dereferencable because `self` is a safe reference.
// The returned pointer is safe because impls of `SliceIndex` have to guarantee that it is.
unsafe { &*index.get_unchecked(self) }
}
Ce contrat est essentiel car s’il est violé, cela peut introduire des comportements indéfinis dans du code Rust supposé sûr.
Utiliser une variable globale externe en Rust
Utiliser static
dans le bloc extern
lié à la bibliothèque externe :
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
Utiliser une fonction C en Rust
On déclare la fonction dans un bloc extern "C"
. Tous les appels à celle-ci sont nécessairement unsafe
.
La déclaration en C :
// header.h
int git_libgit2_init();
Le code Rust :
// main.rs
extern "C" {
pub fn git_libgit2_init() -> c_int;
}
fn main() {
unsafe {
git_libgit2_init();
};
}
Fonctions variadiques
On peut utiliser les fonctions variadiques du C en Rust :
extern {
fn foo(x: i32, ...);
}
fn main() {
unsafe {
foo(10, 20, 30, 40, 50);
}
}
Utiliser une fonction Rust en C
On utilise #[no_mangle]
et extern "C"
comme vu précédemment.
#[no_mangle]
pub extern "C" fn magic_number() -> c_int {
42
}
Et on ajoute bien évidemment du côté C :
extern int magic_number();
Panique
A l’extérieur de Rust, les paniques causent un comportement indéfini, il faut donc les éviter. S’il faut quand même paniquer, utiliser catch_unwind
.
Pointeur nul
Beaucoup de types Rust ne peuvent pas être nuls, notamment les références et les pointeurs de fonction. Ce n’est en revanche pas le cas en C. Le langage Rust donne une approche simple pour s’interfacer avec le C dans cette situation : pour faire simple, utiliser une Option
. La structure sera bien optimisée avec None
correspondant à la valeur nulle et Some
correspondant à la valeur non-nulle.
C | Rust |
---|---|
int (*)(int) | Option<extern "C" fn(c_int) -> c_int> |
data_t * | Option<*mut data_t> |
Libération de la mémoire
On utilise le trait Drop
pour appeler les fonctions étrangères qui conviennent à la libération de la mémoire.
Exemple :
impl Drop for Repository {
fn drop(&mut self) {
unsafe {
raw::git_repository_free(self.raw);
}
}
}
Structure opaque
Si les entêtes en C décrivent un type opaque :
typedef struct Foo Foo;
Ou bien en argument de fonction :
void bar(void *foo);
Alors on utilise dans le code Rust :
#[repr(C)] pub struct Foo { _private: [u8; 0] }
extern "C" {
pub fn bar(arg: *mut Foo);
}
On manipulera donc uniquement ces structures avec des pointeurs bruts.