Интерфейс внешних функций (foreign function interface)
Введение
В данном руководстве в качестве примера мы будем использовать
snappy, библиотеку для сжатия/распаковки
данных. Мы реализуем Rust-интерфейс к этой библиотеке через вызов внешних
функций. Rust в настоящее время не в состоянии делать вызовы напрямую в
библиотеки C++, но snappy включает в себя интерфейс C (документирован в
[snappy-c.h
](https://github.com/google/snappy/blob/master/snappy-c.h)).
Ниже приведен минимальный пример вызова внешней функции, который будет скомпилирован при условии, что библиотека snappy установлена:
# #![feature(libc)]
extern crate libc;
use libc::size_t;
#[link(name = "snappy")]
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("максимальный размер сжатого буфера длиной 100 байт: {}", x);
}
Блок extern
содержит список сигнатур функций из внешней библиотеки, в данном
случае для C ABI (application binary interface; двоичный интерфейс приложений)
данной платформы. Чтобы указать, что программу нужно компоновать с библиотекой
snappy, используется атрибут #[link(...)]
. Благодаря этому, символы будут
успешно разрешены.
Предполагается, что внешние функции могут быть небезопасными, поэтому их вызовы
должны быть обёрнуты в блок unsafe {}
как обещание компилятору, что все внутри
этого блока в действительности безопасно. Библиотеки C часто предоставляют
интерфейсы, которые не являются потоко-безопасными. И почти любая функция,
которая принимает в качестве аргумента указатель, не может принимать любое
входное значений, поскольку указатель может быть висячим; сырые указатели
выходят за пределы безопасной модели памяти в Rust.
При объявлении типов аргументов для внешней функции, компилятор Rust не может проверить, является ли данное объявление корректным. Поэтому важно правильно указать тип привязываемой функции — иначе ошибка обнаружится только во время исполнения.
Блок extern
может быть распространён на весь API snappy:
# #![feature(libc)]
extern crate libc;
use libc::{c_int, size_t};
#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}
# fn main() {}
Создание безопасного интерфейса
Сырой C API (application programming interface; интерфейс программирования приложений) необходимо обернуть, чтобы обеспечить безопасность памяти. Тогда мы сможем использовать концепции более высокого уровня, такие как векторы. Библиотека может выборочно открывать только безопасный, высокоуровневый интерфейс и скрывать небезопасные внутренние детали.
Оборачивание функций, которые принимают в качестве входных параметров буферы,
включает в себя использование модуля slice::raw
для управления векторами Rust
как указателями на память. Векторы Rust представляют собой гарантированно
непрерывный блок памяти. Длина — это количество элементов, которое в настоящее
время содержится в векторе, а ёмкость — общее количество выделенной памяти в
элементах. Длина меньше или равна ёмкости.
# #![feature(libc)]
# extern crate libc;
# use libc::{c_int, size_t};
# unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
# fn main() {}
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}
Обёртка validate_compressed_buffer
использует блок unsafe
, но это
гарантирует, что её вызов будет безопасен для всех входных данных, поскольку
модификатор unsafe
отсутствует в сигнатуре функции. Т.е. небезопасность скрыта
внутри функции и не видна вызывающему.
Функции snappy_compress
и snappy_uncompress
являются более сложными, так как
должен быть выделен буфер для хранения выходных данных.
Функция snappy_max_compressed_length
может быть использована для выделения
вектора максимальной ёмкости, требуемой для хранения сжатых выходных данных.
Затем этот вектор может быть передан в функцию snappy_compress
в качестве
выходного параметра. Ещё один параметр передается, чтобы получить настоящую
длину после сжатия и установить соответствующую длину вектора.
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
# d: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
# fn main() {}
pub fn compress(src: &[u8]) -> Vec<u8> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen = snappy_max_compressed_length(srclen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
snappy_compress(psrc, srclen, pdst, &mut dstlen);
dst.set_len(dstlen as usize);
dst
}
}
Распаковка аналогична, потому что snappy хранит размер несжатых данных как часть
формата сжатия, и snappy_uncompressed_length
будет возвращать точный размер
необходимого буфера.
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_uncompress(compressed: *const u8,
# compressed_length: size_t,
# uncompressed: *mut u8,
# uncompressed_length: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_uncompressed_length(compressed: *const u8,
# compressed_length: size_t,
# result: *mut size_t) -> c_int { 0 }
# fn main() {}
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen: size_t = 0;
snappy_uncompressed_length(psrc, srclen, &mut dstlen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
dst.set_len(dstlen as usize);
Some(dst)
} else {
None // SNAPPY_INVALID_INPUT
}
}
}
Для справки, примеры, используемые здесь, также доступны в библиотеке на GitHub.
Деструкторы
Внешние библиотеки часто передают владение ресурсами в вызывающий код. Когда это происходит, мы должны использовать деструкторы Rust, чтобы обеспечить безопасность и гарантировать освобождение этих ресурсов (особенно в случае паники).
Чтобы получить более подробную информацию о деструкторах, смотрите типаж Drop.
Обратные вызовы функций Rust кодом на C (Callbacks from C code to Rust
functions)
Некоторые внешние библиотеки требуют использование обратных вызовов для передачи
вызывающей стороне отчета о своем текущем состоянии или промежуточных данных. Во
внешнюю библиотеку можно передавать функции, которые были определены в Rust. При
создании функции обратного вызова, которую можно вызывать из C кода, необходимо
указать для нее спецификатор extern
, за котороым следует подходящее соглашение
о вызове.
Затем функция обратного вызова может быть передана в библиотеку C через регистрационный вызов, и уже затем может быть вызвана оттуда.
Простой пример:
Код на Rust:
extern fn callback(a: i32) {
println!("Меня вызывают из C со значением {0}", a);
}
#[link(name = "extlib")]
extern {
fn register_callback(cb: extern fn(i32)) -> i32;
fn trigger_callback();
}
fn main() {
unsafe {
register_callback(callback);
trigger_callback(); // Активация функции обратного вызова
}
}
Код на C:
typedef void (*rust_callback)(int32_t);
rust_callback cb;
int32_t register_callback(rust_callback callback) {
cb = callback;
return 1;
}
void trigger_callback() {
cb(7); // Вызовет callback(7) в Rust
}
В этом примере функция main()
в Rust вызовет функцию trigger_callback()
в C,
которая, в свою очередь, выполнит обратный вызов функции callback()
в Rust.
Обратные вызовы, адресованные объектам Rust (Targeting callbacks to Rust
objects)
Предыдущий пример показал, как глобальная функция может быть вызвана из C кода. Однако зачастую желательно, чтобы обратный вызов был адресован конкретному объекту в Rust. Это может быть объект, который представляет собой обертку для соответствующего объекта C.
Такое поведение может быть достигнуто путем передачи небезопасного указателя на объект в библиотеку C. После чего библиотека C сможет передавать указатель на объект Rust при обратном вызове. Это позволит получить небезопасный доступ к объекту Rust, на которой сослались в обратном вызове.
Код на Rust:
#[repr(C)]
struct RustObject {
a: i32,
// другие поля
}
extern "C" fn callback(target: *mut RustObject, a: i32) {
println!("Меня вызывают из C со значением {0}", a);
unsafe {
// Меняем значение в RustObject на значение, полученное через функцию обратного вызова
(*target).a = a;
}
}
#[link(name = "extlib")]
extern {
fn register_callback(target: *mut RustObject,
cb: extern fn(*mut RustObject, i32)) -> i32;
fn trigger_callback();
}
fn main() {
// Создаём объект, на который будем ссылаться в функции обратного вызова
let mut rust_object = Box::new(RustObject { a: 5 });
unsafe {
register_callback(&mut *rust_object, callback);
trigger_callback();
}
}
Код на C:
typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;
int32_t register_callback(void* callback_target, rust_callback callback) {
cb_target = callback_target;
cb = callback;
return 1;
}
void trigger_callback() {
cb(cb_target, 7); // Вызовет callback(&rustObject, 7) в Rust
}
Асинхронные обратные вызовы
В приведённых примерах обратные вызовы выполняются как непосредственная реакция на вызов функции внешней библиотеки на C. Для выполнения обратного вызова поток исполнения переключался из Rust в C, а затем снова в Rust, но, в конце концов, обратный вызов выполнялся в том же потоке, из которого была вызвана функция, инициировавшая обратный вызов.
Более сложная ситуация — это когда внешняя библиотека порождает свои собственные
потоки и осуществляет обратные вызовы из них. В этих случаях доступ к структурам
данных Rust из обратных вызовов особенно опасен, и поэтому нужно использовать
соответствующие механизмы синхронизации. Помимо классических механизмов
синхронизации, таких как мьютексы, в Rust есть еще одна возможность:
использовать каналы (std::sync::mpsc::channel
), чтобы направить данные из
потока C, который выполнял обратный вызов, в поток Rust.
Если асинхронный обратный вызов адресован конкретному объекту в адресном пространстве Rust, то необходимо, чтобы обратные вызовы не выполнялись библиотекой C после уничтожения этого объекта Rust. Для этого следует, во-первых, проектировать библиотеку таким образом, чтобы отмена регистрации обратного вызова гарантировала, что он больше не будет выполняться. Во-вторых, нужно отменить регистрацию обратного вызова в деструкторе объекта Rust, которому адресован обратный вызов.
Компоновка
Атрибут link
для блоков extern
предоставляет rustc
основные инструкции
относительно того, как он должен компоновать нативные библиотеки. На данный
момент есть две общепринятых формы записи атрибута link
:
#[link(name = "foo")]
#[link(name = "foo", kind = "bar")]
В обоих этих случаях foo
— это имя нативной библиотеки, с которой мы
компонуемся. Во втором случае bar
— это тип нативной библиотеки, с которой
происходит компоновка. В настоящее время rustc
известны три типа нативных
библиотек:
- Динамические —
#[link(name = "readline")]
- Статические —
#[link(name = "my_build_dependency", kind = "static")]
- Фреймворки —
#[link(name = "CoreFoundation", kind = "framework")]
Обратите внимание, что фреймворки доступны только для OSX.
Различные значения kind
нужны, чтобы определить, как компоновать нативную
библиотеку. С точки зрения компоновки, компилятор Rust создает две разновидности
артефактов: промежуточный (rlib/статическая библиотека) и конечный (динамическая
библиотека/исполняемый файл). (Прим. переводчика: rlib — это формат статической
библиотеки с метаданными в формате Rust) Зависимости от нативных динамических
библиотек и фреймворков распространяются дальше, пока не дойдут до конечного
артефакта, а от статических библиотек — нет.
Вот несколько примеров того, как эта модель может быть использована:
-
Нативная зависимость при сборке. Иногда написанный на Rust код необходимо состыковать с некоторым кодом на C/C++, но распространение C/C++ кода в формате библиотеки вызывает дополнительные трудности. В этом случае, код будут упакован в
libfoo.a
, а затем контейнер Rust должен будет объявить зависимость с помощью#[link(name = "foo", kind = "static")]
.Независимо от типа результата (промежуточный или конечный) контейнера, нативная статическая библиотека будет включена в него на выходе, поэтому нет необходимости в распространении этой нативной статической библиотеки отдельно.
-
Обычная динамическая зависимость. Общие системные библиотеки (такие, как
readline
) доступны на большом количестве систем, и статическую копию этих библиотек часто сложно найти. Когда такая зависимость включена в контейнер Rust, промежуточные артефакты (например, rlib'ы) не будут компоноваться с библиотекой, но когда rlib включается в состав конечного артефакта (например, исполняемый файл), нативная библиотека будет прикомпонована.
На OSX, фреймворки ведут себя так же, как и динамические библиотеки.
Небезопасные блоки
Некоторые операции, такие как разыменование небезопасных указателей или вызов функций, которые были отмечены как небезопасные, разрешено использовать только внутри небезопасных блоков. Небезопасные блоки изолируют опасные ситуации и дают гарантии компилятору, что опасности не вытекут за пределы блока.
Небезопасные функции же, наоборот, показывают свою опасность всем. Небезопасная функция записывается в виде:
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
Эта функция может быть вызвана только из блока unsafe
или из другой unsafe
функции.
Доступ к внешним глобальным переменным
Внешние API довольно часто экспортируют глобальные переменные, которые могут
быть использованы, например, для отслеживания глобального состояния. Для того,
чтобы получить доступ к этим переменным, нужно объявить их в блоке extern
,
используя ключевое слово static
:
# #![feature(libc)]
extern crate libc;
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
fn main() {
println!("You have readline version {} installed.",
rl_readline_version as i32);
}
Кроме того, возможно, вам потребуется изменить глобальное состояние,
предоставленное внешним интерфейсом. Для этого при объявлении статических
переменных может быть добавлен модификатор mut
, чтобы была возможность
изменять их.
# #![feature(libc)]
extern crate libc;
use std::ffi::CString;
use std::ptr;
#[link(name = "readline")]
extern {
static mut rl_prompt: *const libc::c_char;
}
fn main() {
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
}
Обратите внимание, что любое взаимодействие с static mut
небезопасно — как
чтение, так и запись. Работа с изменяемым глобальным состоянием требует
значительно большей осторожности.
Соглашение о вызове внешних функций
Большинство внешнего кода предоставляет C ABI. И Rust при вызове внешних функций по умолчанию использует соглашение о вызове C для данной платформы. Но некоторые внешние функции, в первую очередь Windows API, используют другое соглашение о вызове. Rust обеспечивает способ указать компилятору, какое именно соглашение использовать:
# #![feature(libc)]
extern crate libc;
#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {
fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}
# fn main() { }
Это указание относится ко всему блоку extern
. Вот список поддерживаемых
ограничений для ABI:
stdcall
aapcs
cdecl
fastcall
Rust
rust-intrinsic
system
C
win64
Большинство ABI в этом списке не требуют пояснений, но ABI system
может
показаться немного странным. Он выбирает такое ABI, которое подходит для
взаимодействия с нативными библиотеками данной платформы. Например, на платформе
win32 с архитектурой x86, это означает, что будет использован ABI stdcall
.
Однако, на windows x86_64 используется соглашение о вызове C
, поэтому в этом
случае будет использован C
ABI. Это означает, что в нашем предыдущем примере
мы могли бы использовать extern "system" { ... }
, чтобы определить блок для
всех windows систем, а не только для x86.
Взаимодействие с внешним кодом
Rust гарантирует, что размещение полей struct
совместимо с представлением в C
только в том случае, если к ней применяется атрибут #[repr(C)]
. Атрибут
#[repr(C, packed)]
может быть использован для размещения полей структуры без
выравнивания. Атрибут #[repr(C)]
также может быть применен и к перечислениям.
Владеющие упаковки в Rust (Box<T>
) используют указатели, не допускающие
нулевое значение (non-nullable), как дескрипторы содержащихся в них объектов.
Тем не менее, эти дескрипторы не должны создаваться вручную, так как они
управляются внутренними средствами выделения памяти. Ссылки можно без риска
считать ненулевыми указателями непосредствено на тип. Однако нарушение правил
проверки заимствования или изменяемости может быть небезопасным. Но компилятор
не может сделать так много предположений о сырых указателях. Например, он не
полагается на настоящую неизменяемость данных под неизменяемым сырым указателем.
Поэтому используйте сырые указатели (*
), если вам необходимо намеренно
нарушить правила (но так, что при этом всё работает). Это нужно, чтобы
компилятор «случайно» не предположил относительно ссылок чего-то, что мы
собираемся нарушать (возможно, нам нужны несколько указателей с правом
изменения, что не допускается обычными ссылками).
Векторы и строки совместно используют одну и ту же базовую cхему размещения
памяти и утилиты, доступные в модулях vec
и str
, для работы с C API. Однако,
строки не завершаются нулевым байтом, \0
. Если вам нужна строка, завершающаяся
нулевым байтом, для совместимости с C, вы должны использовать тип CString
из
модуля std::ffi
.
Стандартная библиотека включает в себя псевдонимы типов и определения функций
для стандартной библиотеки C в модуле libc
, и Rust компонует libc
и libm
по умолчанию.
Оптимизация указателей, допускающих нулевое значение
(The nullable pointer optimization)
Некоторые типы по определению не могут быть null
. Это ссылки (&T
, &mut T
),
упаковки (Box<T>
), указатели на функции (extern "abi" fn()
). При
взаимодействии же с С часто используются указатели, которые могут быть null
.
Как особый случай — обобщенный enum
, который содержит ровно два варианта, один
из которых не содержит данных, а другой содержит одно поле. Такое использование
перечисления имеет право на «оптимизацию указателя, допускающего нулевое
значение». Когда создан экземпляр такого перечисления с одним из не-обнуляемых
типов, то он представляет собой ненулевой указатель для варианта, содержащего
данные, и нулевой — для варианта без данных. Таким образом, Option<extern "C" fn(c_int) -> c_int>
— это представление указателя на функцию, допускающего
нулевое значение, и совместимого с C ABI.
Вызов кода на Rust из кода на C
Вы можете скомпилировать код на Rust таким образом, чтобы он мог быть вызван из кода на C. Это довольно легко, но требует нескольких вещей:
#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
# fn main() {}
extern
указывает, что эта функцию придерживается соглашения о вызове C, как
описано выше в разделе
«Соглашение о вызове внешних функций».
Атрибут no_mangle
выключает изменение имён, применяемое в Rust, чтобы было
легче компоноваться с этим кодом.