20. FFI

20. FFI

20. FFI

20 декември 2017

Административни неща

Преговор

Foreign Function Interface (ffi)

Викане на C функции от Rust

// main.c

int add_in_c(int a, int b) {
    return a + b;
} 
// main.rs

use std::os::raw::c_int;

extern {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
}

Викане на C функции от Rust

extern

Викане на C функции от Rust

extern

Външни функции са unsafe, защото компилатора не може да гарантира, че работят правилно

extern {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
}

fn main() {
    let res = unsafe { add_in_c(1, 2) };
    println!("{}", res);
}

Викане на C функции от Rust

Calling convention

Calling conventions се задават на extern блока. По подразбиране е `C`

extern "C" {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
}

extern "system" {
    fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> c_int;
}

Викане на C функции от Rust

Calling convention

Викане на C функции от Rust

Calling convention

Calling convention-а задължително трябва да съвпада с това как е компилирана функцията в библиотеката

"Rust" - каквото rustc използва за нормалните функции. Не се използва при ffi.

"C" - каквото C компилаторът използва по подразбиране. Това почти винаги е правилното, освен ако не е казано друго изрично или библиотеката е компилирана с друг компилатор.

"system" - използва се за системни функции (на Windows). Или просто използвайте crate-а winapi.

Викане на C функции от Rust

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" ...
  = note: target/debug/deps/ffi-64ac5f7deef7d1bb.ffi0.rcgu.o: In function `ffi::main':
          src/main.rs:8: undefined reference to `add_in_c'
          collect2: error: ld returned 1 exit status 

Очаквано, не сме казали на компилатора къде да намери функцията

Linking

Ръчният начин

Static linking

# компилираме C кода до math.lib / libmath.a

cargo rustc -- -L . -l math           # или
cargo rustc -- -L . -l static=math 

Dynamic linking

# компилираме C кода до math.dll / libmath.so

cargo rustc -- -L . -l math           # или
cargo rustc -- -L . -l dylib=math 

Linking

Правилният начин

#[link(name="math")]
extern {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
}

Linking

Правилният начин

cargo rustc -- -L . 

Linking

Странности

Няма значение на кой блок е поставен #[link] атрубута

#[link(name="foo")]
#[link(name="bar")]
extern {}

extern {
    fn foo_init();
    fn foo_stuff(x: c_int);
}

extern {
    fn bar_init();
    fn bar_stuff() -> c_int;
}

Build scripts

Cargo предоставя възможност за изпълняване на скрипт преди компилиране на crate-a

http://doc.crates.io/build-script.html

Build scripts

// Cargo.toml

[package]
name = "ffi"
version = "0.1.0"
authors = ["..."]
build = "build.rs"


// build.rs

fn main() {
    // rust code
} 

Build scripts

// Cargo.toml

[build-dependencies]
... 

Build scripts

Build scripts

// build.rs

fn main() {
    println!("cargo:rustc-link-search=.");
}
cargo build
cargo run 

Callbacks

// main.c

typedef int (*callback)(int);

int add_in_c(int a, int b) {
    return a + b;
}

int apply(int a, callback op) {
    return op(a);
} 
// main.rs

#[link(name="math")]
extern "C" {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
    fn apply(a: c_int, op: fn(c_int) -> c_int) -> c_int;
}

fn cube(x: i32) -> i32 { x * x * x }

fn main() {
    println!("{}", unsafe { apply(11, cube) });
}

Callbacks

$ cargo build

warning: found function pointer with Rust calling convention in foreign module;
         consider using an `extern` function pointer
 --> src/main.rs:7:28
   |
 7 |     fn apply(a: c_int, op: fn(c_int) -> c_int) -> c_int;
   |                            ^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(improper_ctypes)] on by default 

Callbacks

Callbacks

// main.rs

#[link(name="math")]
extern "C" {
    fn add_in_c(a: c_int, b: c_int) -> c_int;
    fn apply(a: c_int, op: extern fn(c_int) -> c_int) -> c_int;
}

extern fn cube(x: i32) -> i32 { x * x * x }

fn main() {
    println!("{}", unsafe { apply(11, cube) });
}
1331 

Panics

A panic! across an FFI boundary is undefined behavior

Когато подаваме или експортираме rust функции трябва да се подсигурим, че те не могат да се панират. В тази ситуация е удобно да се използва catch_unwind

extern fn oh_no() -> i32 {
    let result = catch_unwind(|| {
        panic!("Oops!");
    });

    match result {
        Ok(_) => 0,
        Err(_) => 1,
    }
}

Други неща

Writing wrappers

Много често е удобно да напишем "rusty" интерфейс към библиотеката

extern crate libc;
use libc::{c_int, c_size_t};

#[link(name="math")]
extern {
    fn math_array_sum(arr: *const c_int, len: c_size_t) -> c_int;
}

/// Safe wrapper
pub fn array_sum(arr: &[c_int]) {
    unsafe { math_array_sum(arr.as_ptr(), arr.len()) }
}

Writting wrappers

Други

Споделяне на структури

Структурите в rust нямат определено подреждане на полетата.

// main.c

struct FooBar {
    int foo;
    short bar;
};

void foobar(FooBar x) {
    // ...
} 
extern {
    fn foobar(x: FooBar);
}

struct FooBar {
    foo: c_int,
    bar: c_short,
}

Споделяне на структури

warning: found struct without foreign-function-safe representation annotation in foreign module,
         consider adding a #[repr(C)] attribute to the type
 --> src/main.rs:9:18
   |
 9 |     fn foobar(x: FooBar);
   |                  ^^^^^^
   |
   = note: #[warn(improper_ctypes)] on by default 

Споделяне на структури

За да споделим структура между Rust и C трябва да забраним на компилатора да размества полетата с #[repr(C)]

// main.c

struct FooBar {
    int foo;
    short bar;
};

void foobar(FooBar x) {
    // ...
} 
extern {
    fn foobar(x: FooBar);
}

#[repr(C)]
struct FooBar {
    foo: c_int,
    bar: c_short,
}

Споделяне на структури

За плътно пакетирани структури се използва #[repr(C, packed)]

// main.c

// gcc синтаксис
struct __atribute((__packed__)) FooBar {
    int foo;
    short bar;
};

void foobar(FooBar x) {
    // ...
} 
extern {
    fn foobar(x: FooBar);
}

#[repr(C, packed)]
struct FooBar {
    foo: c_int,
    bar: c_short,
}

Споделяне на низове

Споделяне на низове

CString

Низ със собственост, съвместим със C.
Не съдържа нулеви байтове ('\0') във вътрешността и завършва на терминираща нула.

use std::ffi::CString;

// създава се от неща които имплементират Into<Vec<u8>>,
// в това число &str и String
let hello = CString::new("Hello!").unwrap();

unsafe {
    print(hello.as_ptr());
}

Споделяне на низове

CString

Какъв е проблема?

extern {
    fn print(s: *const c_char);
}

unsafe {
    print(CString::new("Hello!").unwrap().as_ptr());
}

Споделяне на низове

CString

Работим с голи указатели, а не с референции. Трябва да се погрижим паметта да живее достатъчно!

let hello = CString::new("Hello!").unwrap();
let ptr = hello.ptr();                                // `ptr` е валиден докато `hello` е жив
unsafe { print(ptr) };                                // Ок

let ptr = CString::new("Hello!").unwrap().as_ptr();   // временния CString се деалокира
unsafe { print(ptr) };                                // подаваме dangling pointer

Споделяне на низове

CStr

Option and the "nullable pointer optimization"

// c

typedef int (*callback)(int);

// callback can be a function pointer or NULL
void register(f: callback);



// rust
extern "C" {
    /// Registers the callback.
    fn register(cb: Option<extern "C" fn(c_int) -> c_int>);
}

Opaque types

Много често C код използва opaque types

// c

struct Foo;

Foo* init();
int stuff(Foo* foo); 

Opaque types

За да представим такъв тип в rust можем да използваме празен enum

Не можем да създадем променлива от този тип, защото enum-а няма варианти

enum Foo {}

extern {
    fn init() -> *const Foo;
    fn stuff(foo: *const Foo) -> c_int;
}

Компилиране до библиотека

До сега сме правили библиотеки за Rust чрез cargo new --lib. При компилация този вид crate ни дава rlib и има следния Cargo.toml

[package]
name = "project_name"
version = "0.1.0"
authors = ["..."]

[dependencies]

Компилиране до библиотека

Това е достатъчно когато правим crate за Rust екосистемата, но понякога се налага да създадем специфично статична или динамична библиотека

Компилиране до библиотека

В този случай може да използвате Cargo.toml, за да укажете това

[package]
name = "project_name"
version = "0.1.0"

[lib]
crate-type = ["..."]

[dependencies]

Компилиране до библиотека

на мястото на триеточието може да поставим следните типове

Компилиране до библиотека

При компилация с cargo файловете се намират в target/${target_type}

bindgen crate

Bindgen

Ресурси

Въпроси