Содержание


Rust — новый язык программирования: Часть 19. Интерфейс с другими языками программирования

Comments

В предыдущих статьях цикла был сделан общий обзор языка программирования Rust, обсуждались основы синтаксиса: были описаны простейшие компоненты (переменные, простые типы данных, операторы и расширения), управляющие конструкции и составные структуры данных, функции и деструкторы. Особое внимание было уделено модели памяти в целом, концепции владения объектами, общим принципам управления памятью, использованию собственных и управляемых блоков общей памяти, а также заимствованных указателей. Далее описывались средства ввода/вывода, векторы и строки, контейнеры и итераторы, замыкания и do-выражения. Темой двух отдельных статей была объектная система Rust, трэйтов, их методов, а также обобщённые функции. Далее обсуждалась модульная система языка Rust, основанная на крэйтах, кроме того, был сделан краткий обзор стандартной библиотеки. В трёх последних статьях описывалась организация многопоточности в Rust — а именно, задачи и различные варианты передачи данных между ними, вопросы обработки ошибок и организация двунаправленного соединения для обмена данными, а также подсистема макрокоманд. В данной статье, завершающей цикл, рассматривается интерфейс с функциями из других языков программирования.

Поддержка взаимодействия с функциями, написанными на других языках (FFI — foreign function interface), уже стала стандартом де-факто во многих современных языках программирования. Здесь Rust также не является исключением, и такое взаимодействие обеспечивает. В статье рассматривается интерфейс с «чужими» функциями главным образом на примере языка C, по той причине, что этот интерфейс наиболее хорошо проработан и в стандартной библиотеке Rust имеется модуль std::libc для его поддержки.

1. Основы FFI-интерфейса в Rust

Осваивать FFI-интерфейс гораздо проще и быстрее на практических примерах, при этом примеры должны быть доступными для понимания. В данном случае для организации взаимодействия с Rust-кодом выбрана простая и относительно небольшая по размеру библиотека Snappy, обеспечивающая сжатие (упаковку) и распаковку данных. Snappy написана на C++, но имеет полный комплект привязок к языку C, а также средства взаимодействия с другими языками (C#, Java, Python, Lua, Go и др.). В настоящий момент Rust не имеет возможности напрямую обращаться с вызовами в библиотеку C++, но, как было сказано выше, библиотека Snappy включает в себя полноценный C-интерфейс, документированный в заголовочном файле snappy-c.h, что позволяет использовать Snappy совместно с Rust-программами.

C-интерфейс библиотеки Snappy лаконичен и представлен всего лишь пятью функциями:

snappy_status snappy_compress( const char *input, size_t input_length, char *compressed, 
size_t *compressed_length );

Эта функция принимает данные из буфера input (размер исходных данных input_length-1) и записывает результат сжатия в буфер compressed (размер сжатых данных compressed_length).

snappy_status snappy_uncompress( const char *compressed, size_t compressed_length, 
char *uncompressed, size_t *uncompressed_length );

Эта функция выполняет распаковку (декомпрессию) сжаты ранее данных. Предназначение параметров очевидно из их имён.

size_t snappy_max_compressed_length( size_t source_length );

Функция возвращает максимальный возможный размер сжатия исходных данных, имеющих размер source_length.

snappy_status snappy_uncompressed_length( const char *compressed, size_t 
compressed_length, size_t *result );

Буфер сжатых данных compressed — результат работы функции snappy_compress(). Данная функция в обычном режиме возвращает положительный результат (SNAPPY_OK) и записывает размер распакованных данных в параметр *result. При возникновении ошибки при анализе сжатых данных возвращает SNAPPY_INVALID_INPUT.

snappy_status snappy_validate_compressed_buffer( const char *compressed, 
size_t compressed_length );

Функция проверяет содержимое буфера compressed — может ли он быть корректно распакован — и при положительном результате возвращает SNAPPY_OK, иначе — SNAPPY_INVALID_INPUT. Сама операция распаковки данных не выполняется.

Упоминаемые выше специальные возвращаемые значения (как характеристики состояния) определены как перечисление:

typedef enum { SNAPPY_OK=0, SNAPPY_INVALID_INPUT=1, 
SNAPPY_BUFFER_TOO_SMALL=2 } snappy_status;

Теперь, при условии, что библиотека Snappy установлена в системе, можно написать небольшой пример, демонстрирующий вызов функции из другого языка программирования (FFI-вызов), как показано в листинге 1.

Листинг 1. Пример использования FFI-интерфейса — вызов C-функции
use std::libc::size_t;

#[link_args = "-lsnappy"]
extern {
  fn snappy_max_compressed_length( source_length: size_t ) -> size_t;
}

#[fixed_stack_segment]
fn main() {
  let x = unsafe { snappy_max_compressed_length( 100 ) };
  println( fmt!( "Максимальная длина сжатых данных при размере исходного буфера 
100 байтов: %?", x ) );
}

Атрибут #[link_args] используется для того, чтобы сообщить о необходимости подключения при сборке программы внешней библиотеки snappy. Блок extern представляет собой список сигнатур функций, содержащихся во внешней библиотеке, написанной на другом языке; в данном случае это двоичный интерфейс C (C ABI) для текущей платформы.

Предполагается, что все внешние функции из других языков программирования являются небезопасными, и их вызовы необходимо размещать в блоке unsafe { }, чтобы оповестить об этом компилятор. Достаточно часто C-библиотеки предоставляют интерфейсы, которые не могут обеспечить безопасность при многопоточном режиме, а кроме того, почти любая функция, принимающая указатель в качестве аргумента, никоим образом не может считаться безопасной с точки зрения модели безопасной памяти Rust по очевидным причинам.

Директива #[fixed_stack_segment] перед функцией main() сообщает компилятору Rust о том, что при выполнении main() может быть запрошен сегмент стека «очень большого размера». Более подробная информация об управлении стеком будет дана в следующих разделах.

Следует обратить особое внимание на тот факт, что при объявлении типов аргументов для внешней функции на другом языке компилятор Rust не проверяет корректность этих объявлений, и неправильное объявление аргументов внешних функций не вызывает ошибок при компиляции, но может привести к ошибке во время выполнения, поэтому вся ответственность ложится на программиста.

Само собой разумеется, что блок extern можно расширить таким образом, чтобы он включал в себя все C-функции программного интерфейса библиотеки snappy, как показано в листинге 2.

Листинг 2. Блок extern, объявляющий полный ffi-интерфейс с библиотекой Snappy
use std::libc::{c_int, size_t};

#[link_args = "-lsnappy"]
extern {
  fn snappy_compress( input: *u8, input_length: size_t, compressed: *mut u8, 
compressed_length: *mut size_t ) -> c_int;
  fn snappy_uncompress( compressed: *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: *u8, compressed_length: size_t, 
result: *mut size_t ) -> c_int;
  fn snappy_validate_compressed_buffer( compressed: *u8, compressed_length: 
size_t ) -> c_int;
}

2. Создание безопасного FFI-интерфейса

В предыдущем разделе отмечалось, что при работе с функциями C-интерфейса необходимо их «обёртывание» в блоки unsafe для того, чтобы компилятор мог обеспечить безопасное использование памяти. Но помимо этого, есть возможность организовать взаимодействие так, чтобы библиотека предъявляла только безопасные элементы интерфейса высокого уровня и скрывала все внутренние детали реализации, которые не могут считаться безопасными.

Создание обёрток для функций, принимающих в качестве аргументов буферы, предполагает использование модуля vec::raw для работы с Rust-векторами как с указателями на блоки памяти. В Rust векторы всегда, при любых обстоятельствах представляют собой непрерывные блоки памяти. Длина вектора — это количество элементов, которые он содержит в текущий момент, а мощность или ёмкость [capacity] вектора — это макисмальное количество элементов, которые можно разместить в выделенной вектору памяти. Таким образом, длина меньше или равна мощности (ёмкости). В листинге 3 приведён пример функции-обёртки, позволяющей интерпретировать буфер, принимаемый C-функцией как вектор Rust.

Листинг 3. Пример функции-обёртки для C-функции
#[fixed_stack_segment]
#[inline(never)]
pub fn validate_compressed_buffer( src: &[u8] ) -> bool {
  unsafe {
    snappy_validate_compressed_buffer( vec::raw::to_ptr(src), src.len() as size_t ) == 0
  }
}

Функция-обёртка validate_compressed_buffer() в данном примере использует блок unsafe, но тем не менее даёт гарантию того, что её вызов безопасен для любых входных данных, поскольку блок unsafe скрыт внутри обёртки, и результат C-функции всегда обрабатывается корректно.

В примере из листинга 3 используются два атрибута: #[fixed_stack_segment] и #[inline(never)]. Цель применения этих атрибутов — обеспечение достаточного размера стека для C-функции при выполнении. Это необходимо, поскольку в Rust, в отличие от C, размещение стека производится не в единой непрерывной области памяти, а вместо этого используется схема сегментированного стека [segmented stack], в которой стек увеличивается или уменьшается только в том случае, когда в этом действительно есть необходимость. Но в C-коде всегда предполагается наличие одного большого стека; именно поэтому сторона, вызывающая C-функции, непременно должна запросить большой сегмент стека, чтобы обеспечить корректную работу C-функции (то есть предотвратить выход за пределы стека).

Компилятор предлагает специальный режим lint, который будет сообщать об ошибке, если C-функция вызывается без атрибута #[fixed_stack_segment]. Более подробно режим lint будет рассмотрен ниже, в соответствующем разделе.

Директива #[inline(never)] информирует компилятор о том, что данная функция никогда не должна становиться встроенной [inline]. Директива #[inline(never)] весьма часто используется в сочетании с атрибутом #[fixed_stack_segment], и это разумное, практичное решение, хотя такая комбинация не является обязательной. Основанием для применения именно такого сочетания является тот факт, что если функция с объявленным атрибутом #[fixed_stack_segment] является (или становится) встроенной [inlined], то код, вызывающий её, также наследует этот атрибут. А это означает, что помимо выделения большого сегмента стека по запросу для вызываемой C-функции (этот сегмент стека будет существовать только во время выполнения данной C-функции), аналогичный большой сегмент стека будет использоваться и в течение всего времени выполнения вызывающего кода, который может находиться в функции main(). Таким образом, во время выполнения всей Rust-программы память будет использоваться нерационально. Это не всегда даёт сугубо отрицательный эффект — например, такая ситуация может даже принести пользу, особенно если функция validate_compressed_buffer() вызывается несколько раз подряд; но это противоречит концептуальной схеме сегментированного стека Rust, стремящейся к сохранению небольших размеров стеков и к экономии адресного пространства памяти.

Обёртки для C-функций snappy_compress() и snappy_uncompress() более сложны, так как требуется не только буфер для входных данных, но и изменяемый буфер для сохранения выходных данных.

C-функция snappy_max_compressed_length() может быть использована для выделения памяти под вектор с максимальной требуемой ёмкостью, способный полностью сохранить получаемый сжатые данные. Этот вектор должен быть передан в функцию snappy_compress() как параметр, предназначенный для сохранения полученного результата. Ещё один параметр для выходных данных — реальная длина данных, сохранённых в выходном буфере после сжатия. Код функции-обёртки для C-функции snappy_compress() приведён в листинге 4.

Листинг 4. Функция-обёртка для C-функции snappy_compress()
pub fn compress( src: &[u8] ) -> ~[u8] {
 #[fixed_stack_segment];
 #[inline(never)];
 unsafe {
  let srclen = src.len() as size_t;
  let psrc = vec::raw::to_ptr( src );
  let mut dstlen = snappy_max_compressed_length( srclen );
  let mut dst = vec::with_capacity( dstlen as uint );
  let pdst = vec::raw::to_mut_ptr( dst );
  snappy_compress( psrc, srclen, pdst, &mut dstlen );
  vec::raw::set_len( &mut dst, dstlen as uint );
  dst
 }
}

Функция-обёртка для декодирования сжатых данных похожа на функцию сжатия, потому что при использовании библиотеки Snappy размер исходных (несжатых) данных сохраняется, как один из элементов записи в формате сжатия, и функция snappy_uncompressed_length() всегда может извлечь требуемый точный размер выходного буфера. Код функции-обёртки для C-функции snappy_uncompress() приведён в листинге 5.

Листинг 5. Функция-обёртка для C-функции snappy_uncompress()
pub fn uncompress( src: &[u8] ) -> Option<~[u8]> {
 #[fixed_stack_segment];
 #[inline(never)];
 unsafe {
  let srclen = src.len() as size_t;
  let psrc = vec::raw::to_ptr( src );
  let mut dstlen: size_t = 0;
  snappy_uncompressed_length( psrc, srclen, &mut dstlen );
  let mut dst = vec::with_capacity( dstlen as uint );
  let pdst = vec::raw::to_mut_ptr( dst );

  if snappy_uncompress( psrc, srclen, pdst, &mut dstlen ) == 0 {
    vec::raw::set_len( &mut dst, dstlen as uint );
    Some( dst )
  } else {
    None // если snappy_uncompressed() вернула SNAPPY_INVALID_INPUT=1
  }
 }
}

3. Автоматические функции-обёртки

Иногда написание функций-обёрток становится слишком тривиальным занятием. Например, внешняя функция не принимает каких-либо аргументов-указателей, и часто оказывается, что нет необходимости в преобразованиях типов. Разумеется, сама функция-обёртка нужна и в этом случае, как минимум для управления сегментированными стеками, но здесь можно воспользоваться преимуществами стандартной макрокоманды externfn!, чтобы частично избавиться от рутинной работы.

В листинге 1 был показан пример объявления внешней функции snappy_max_compressed_length() в блоке extern и последующее использование (вызов) этой функции в блоке unsafe. Сигнатура функции предельно проста: один элементарный аргумент и возвращаемое значение того же типа. Чтобы избежать написания обёртки для этой функции вручную и избавиться от размышлений на тему «а нужна ли в данном конкретном случае директива #[fixed_stack_segment]», можно просто применить макрокоманду externfn!, как показано в листинге 6.

Листинг 6. Использование макро externfn! для автоматизации создания функций-обёрток
use std::libc::size_t;

externfn!( #[link_args = "-lsnappy"]
           fn snappy_max_compressed_length( source_length: size_t ) -> size_t )

fn main() {
  let x = unsafe { snappy_max_compressed_length( 100 ) };
  println( fmt!( "Максимальная длина сжатых данных при размере исходного буфера 
100 байтов: %?", x ) );
}

Из этого примера очевидно, что макрокоманда externfn! полностью заменяет блок extern. После развёртывания макро, то есть после макроподстановки, код будет выглядет так, как показано в листинге 7.

Листинг 7. Код, сгенерированный макрокомандой extrafn! после развёртывания макро
use std::libc::size_t;

// Автоматически сгенерированный код extrafn!
unsafe fn snappy_max_compressed_length( source_length: size_t ) -> size_t {
  #[fixed_stack_segment];
  #[inline(never)];
  return snappy_max_compressed_length( source_length );

  #[link_args = "-lsnappy"]
  extern {
    fn snappy_max_compressed_length( source_length: size_t ) -> size_t;
  }
}
// конец сгенерированного кода

fn main() {
  let x = unsafe { snappy_max_compressed_length( 100 ) };
  println( fmt!( "Максимальная длина сжатых данных при размере исходного буфера 
100 байтов: %?", x ) );
}

4. Сегментные стеки и линтер

При любом вызове функции, внешней по отношению к Rust-коду, по умолчанию встроенный линтер cstack проверяет соблюдение одного из следующих условий:

  1. Вызов выполняется внутри функции, которая была объявлена с директивой #[fixed_stack_segment].
  2. Вызов выполняется внутри функции из блока extern.
  3. Вызов выполняется в стековом замыкании, созданном какой-либо другой безопасной функцией.

Все перечисленные условия обеспечивают работу внешней вызываемой функции с большим сегментом стека, хотя иногда они могут становиться чрезмерно строгими. Если приложение выполняет множество вызовов C-функций, то зачастую лучше разместить атрибут #[fixed_stack_segment] как можно выше в цепочке (иерархии) таких вызовов. Например, компилятор Rust всегда помечает свою основную функцию main как требующую #[fixed_stack_segment]. В таких случаях действия линтера становятся ненужным излишеством, поскольку все вызовы C-функций во время работы компилятора Rust сразу получают в своё распоряжение стек большого размера. Другой пример возникновения подобной ситуации — на системах с 64-битовой архитектурой, где стеки больших размеров выделяются по умолчанию. Если линтер действительно не нужен, то программист может отключить его директивой #[allow(cstack)], после чего три условия, перечисленные выше, проверяться не будут. При необходимости можно также воспользоваться директивой #[warn(cstack)], позволяющей интерпретировать ошибки использования cstack всего лишь как предупреждения.

5. Небезопасные блоки

Некоторые операции, например, разыменование небезопасных указателей или вызов функций, помеченных как небезопасные, допустимы только внутри блоков unsafe, которые собственно и предназначены для изоляции небезопасных фрагментов кода при компиляции.

С другой стороны функции, объявленные небезопасными, становятся общедоступными в любом небезопасном коде. Например, функция

unsafe fn dangerous_operation( ptr: *int ) -> int { *ptr }

может быть вызвана только из блока unsafe или из другой unsafe-функции.

6. Доступ к глобальным объектам других языков

Прикладные программные интерфейсы (API) других языков программирования часто экспортируют глобальные переменные, которые предназначены, например, для отслеживания некоторого общего состояния, глобального по отношению ко всему приложению в целом. Для получения доступа к таким переменным необходимо объявить их в блоке extern с использованием ключевого слова static, как показано в листинге 8.

Листинг 8. Использование глобальной переменной из другого языка
use std::libc;

#[link_args = "-lreadline"]
extern {
  static rl_readline_version: libc::c_int;
}

fn main() {
  println( fmt!( "В системе установлена версия %d readline", rl_readline_version as int )
 );
}

В более сложных ситуациях может потребоваться изменение глобального состояния, то есть, изменение значения, содержащегося во внешней глобальной переменной, предоставляемой API другого языка. В этом случае глобальная переменная должна быть объявлена не только статической, но и изменяемой (mut), чтобы Rust-код мог модифицировать её значение. Пример использования такой переменной продемонстрирован в листинге 9.

Листинг 9. Изменяемая глобальная внешняя переменная в Rust-коде
use std::libc;
use std::ptr;

#[link_args = "-lreadline"]
extern {
  static mut rl_prompt: *libc::c_char;
}

fn main() {
  do "[my_own_shell} $ ".as_c_str |buf| {
    unsafe { rl_prompt = buf; }
    // после приёма строки ввода (команды) она обрабатывается каким-либо образом
    unsafe { rl_prompt = ptr::null(); }
  }
}

В данном примере глобальная внешняя переменная rl_prompt объявлена как изменяемая, что даёт возможность присваивать ей новые значения, считываемые из буфера ввода. По окончании работы rl_prompt присваивается null-указатель, то есть выполняется корректное освобождение ресурса.

7. Взаимодействие с кодом на других языках программирования

Rust даёт полную гарантию того, что размещение структуры (struct) полностью совместимо с аналогичным размещением структуры в языке C с учётом её представления на конкретной платформе. Доступен также атрибут #[packed], позволяющий разместить члены структуры без выравнивания и заполнения. К сожалению, в настоящее время подобная совместимость не обеспечивается для перечисления enum.

Собственные и управляемые блоки памяти Rust используют указатели, которые ни при каких обстоятельствах не могут стать нулевыми, для обращений к содержащимся в них объектам, и такие связи нельзя создать вручную, поскольку они управляются внутренними аллокаторами памяти. Заимствованные указатели предоставляют бóльшую свободу действий, но не всегда гарантируют безопасность, поэтому более предпочтительным является использование обычных незащищённых указателей (*) в тех случаях, когда компилятор не может вывести корректное предположение о безопасности/небезопасности указателей других видов.

Размещение в памяти векторов и строк аналогично, а в модулях vec и str имеются средства для работы с соответствующими программными интерфейсами языка C. При этом следует помнить о том, что в Rust строки не завершаются символом '\0'. Если при взаимодействии с C-кодом требуется строка, завершающаяся NUL-символом, то необходимо пользоваться функцией c_str::to_c_str().

В модуль libc включены все объявления типов и функций из стандартной библиотеки C, и компилятор Rust выполняет связывание со стандартной библиотекой C libc и с математической библиотекой libm по умолчанию.

Заключение

В современном программировании взаимодействие фрагментов кода и модулей, написанных на различных языках программирования, стало весьма актуальной задачей. Rust пока не может похвастать разнообразием программных интерфейсов с другими языками, но, тем не менее, интерфейс с языком C проработан тщательно и детально, а поскольку многие библиотеки C++ предлагают комплект привязок к API C, то можно говорить о том, что Rust вполне способен взаимодействовать и с C++. Судя по темпам развития Rust, реализация интерфейсов с другими языками программирования также не заставит себя долго ждать.

Данная статья завершает цикл, в котором подробно рассматривался новый перспективный язык программирования Rust.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=968028
ArticleTitle=Rust — новый язык программирования: Часть 19. Интерфейс с другими языками программирования
publish-date=04092014