Выпуск 34. Декабрь 2015
← Perl 6-винегрет | Содержание | Обзор CPAN за ноябрь 2015 г. →Использование Rust из Perl
Встраивание Rust в Perl с помощью FFI
Rust — язык системного программирования, развиваемый Mozilla Fundation. Призван заменить собой С. Обеспечивает безопасную работу с памятью и прочие приятные мелочи, которых иногда так не хватает в С, при этом не дает оверхеда по производительности (в теории).
Подробности языковых конструкций я тут расписывать сильно не буду, в целом, все примеры укладываются в первые 4 главы книги по Rust.
Сначала установим Rust последней версии (1.5 на момент написания). Инструкции по установке на все поддерживаемые платформы можно найти на сайте — https://www.rust-lang.org/downloads.html.
Будем считать, что Rust уже установлен и даже работает. Попробуем его использовать из Perl.
Сначала опробуем пример из документации, создадим новый проект с помощью Cargo:
cargo new embed
cd embed
Добавим в Cargo.toml следующее содержимое:
[lib]
name = "embed"
crate-type = ["dylib"]
Этим мы говорим карго, что мы хотим получить библиотеку, а не бинарник и библиотека должна линковаться с кодом на C.
Теперь отредактируем src/lib.rs и приведем его к такому виду:
use std::thread;
#[no_mangle]
pub extern fn process() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
let mut x = 0;
for _ in (0..5_000_000) {
x += 1
}
x
})
}).collect();
for h in handles {
println!("Thread finished with count={}",
h.join().map_err(|_| "Could not join a thread!").unwrap());
}
println!("done!");
}
Это приложение запускает 10 тредов, каждый из которых считает до 5 000 000, собирает от них возвращаемое значение (счетчик x) и выводит это нам.
Соберем его:
cargo build --release
В каталоге target/release у нас появится файл libembed.so (для Mac OS расширение будет dylib, для Win — dll, я все это проделываю на маке, так что библиотеки везде будут с расширением dylib, вам же надо будет поправить под свою систему) — это наша подключаемая библиотека. При формировании библиотеки Cargo автоматически добавляет префикс lib к имени проекта, указанного в name.
Настало время вызвать ее из Perl. Так как XS-биндингов в Perl для Rust еще не существует (ну или я их не нашел), то мы будем использовать FFI::Raw. Этот модуль позволяет из кода на Perl дергать функции в библиотеках на С. Модуль весьма низкоуровневый, но для начала это даже хорошо.
Создадим файл main.pl со следующим содержимым:
#!/usr/bin/env perl
use v5.16;
use FFI::Raw;
my $process = FFI::Raw->new(
'target/release/libembed.dylib', 'process',
FFI::Raw::void,
);
say $process->call();
say "Done!";
Здесь в конструкторе мы указываем путь к нашей библиотеке, имя функции, которую мы хотим использовать, и тип возвращаемого значения. В данном случае функция у нас ничего не возвращает, так что это будет void.
И запустим наш перловый скрипт:
alpha6$ perl ./main.pl
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
Thread finished with count=5000000
done!
Done!
Этот код может валиться с Segmentation fault 11 перед выходом из скрипта. Это связано с выводом с STDOUT из кода на Rust. Видимо, FFI как-то криво обрабатывает закрытие дескрипторов. На маке валится стабильно, на Linux работает без ошибок, на Windows не проверял. Видимо, влияют какие-то особенности платформы.
Вызывать числодробилки из Perl это конечно хорошо, но как насчет передачи параметров функции? Тоже можно, посчитаем факториал.
Приведем src/lib.rs к виду:
#[no_mangle]
pub extern fn fact(x: i32) -> i32 {
let mut fact = 1;
for i in 1..x+1 {
fact *= i;
}
return fact;
}
Здесь мы объявляем публичную функцию fact, которая принимает на вход число в виде 32-битного целого и то же самое возвращает в качестве результата.
Так же обратите внимание: #[no_mangle] надо указывать перед каждой функцией, которую мы хотим экспортировать. Если убрать эту строку и попробовать вызвать рассчет факториала, мы получим ошибку, несмотря на то, что функция объявлена публичной:
alpha6$ perl ./main.pl
dlsym(0x7ffeda40abd0, fact): symbol not found at ./main.pl line 13.
no_mangle говорит компилятору, чтобы он не изменял имя функции во время компиляции, что делает возможным доступ к ней из других языков.
Приведем main.pl к виду:
my $fact_fn = FFI::Raw->new(
'target/release/libembed.dylib', 'fact',
FFI::Raw::int,
FFI::Raw::int,
);
my $fact_val = $fact_fn->call(30);
say "Factorial [$fact_val]";
Сохраняем, запускаем:
alpha6$ perl ./main.pl
Factorial [1409286144]
Perl code done!
Хм, кажется, что-то пошло не так…
В этом примере можно увидеть наглядно различия Perl и языков более низкого уровня. Если посмотреть на результат работы функции, то видно что факториал 30 немножко не является факториалом 30, хотя бы потому что в нем должно быть 22 цифры. При этом просто увеличение разрядности до u64 не поможет, потому что туда он тоже не влезает. В общем, мораль сей басни такова — следите за переполнением переменных, так как компилятор этого не отслеживает, и рантайм-проверок по умолчанию тоже нет.
Впрочем, при изменении разрядности с int до int64, факториал 14 рассчитывается верно, если верить калькулятору, а все, что выше 15 — это уже математика с хитрыми алгоритмами, Rust из коробки с ней работать не умеет.
Еще одна проблема — FFI::Raw не позволяет напрямую передавать и возвращать из внешнего кода что-то сложнее примитивных типов. То есть передать в функцию хеш или вернуть вектор не получится. Но всегда можно работать со структурами С, а перегнать Perl-структуру в сишную — задача элементарная.
Приведем наш lib.rs к виду:
pub struct Args {
init: u32,
by: u32,
}
#[no_mangle]
pub extern fn use_struct(args: &Args) -> u32 {
println!("Args: {} - {}", args.init, args.by);
return args.init;
}
Здесь мы объявляем структуру Args и делаем ее доступной снаружи нашей библиотеки указанием pub. В функции use_struct мы говорим компилятору, что ожидаем на входе указатель на эту структуру. Ну и возвращаем обратно первый элемент структуры.
В перловый код вставим такое:
my $packed = pack('LL', 42, 21);
my $arg = FFI::Raw::MemPtr->new_from_buf($packed, length $packed);
my $foo = FFI::Raw->new(
$shared, 'test_struct',
FFI::Raw::uint,
FFI::Raw::ptr,
);
my $val = $foo->($arg); #Получаем структуру из Rust кода
say "result [$val]";
Здесь мы упаковываем данные для структуры (два числа int32), затем создаем из них объект FFI::Raw::MemPtr, который выделяет область памяти и укладывает туда упакованную стуктуру. Затем мы привязываемся к функции test_struct из нашей библиотеки и говорим, что будем передавать в нее указатель.
Соберем и запустим:
alpha6$ cargo build --release
alpha6$ perl ./main.pl
Args: 42 - 21
result [42]
Поздравляю, мы только что передали структуру данных в нашу библиотеку на Rust. Попробуем теперь сделать с этой структурой что-то более осмысленное.
Создадим счетчик (действие не особо осмысленное, зато показывает как с этим всем работать). Приведем наш lib.rs к виду:
#![allow(non_snake_case)]
use std::mem::transmute;
pub struct Args {
init: u32,
by: u32,
}
pub struct Counter {
val: u32,
by: u32,
}
impl Counter {
pub fn new(args: &Args) -> Counter {
Counter{
val: args.init,
by: args.by,
}
}
pub fn get(&self) -> u32 {
self.val
}
pub fn incr(&mut self) -> u32 {
self.val += self.by;
self.val
}
pub fn decr(&mut self) -> u32 {
self.val -= self.by;
self.val
}
}
#[no_mangle]
pub extern fn createCounter(args: &Args) -> *mut Counter {
let _counter = unsafe { transmute(Box::new(Counter::new(args))) };
_counter
}
#[no_mangle]
pub extern fn getCounterValue(ptr: *mut Counter) -> u32 {
let mut _counter = unsafe { &mut *ptr };
let val = _counter.get();
return val;
}
#[no_mangle]
pub extern fn incrementCounterBy(ptr: *mut Counter) -> u32 {
let mut _counter = unsafe { &mut *ptr };
_counter.incr()
}
#[no_mangle]
pub extern fn decrementCounterBy(ptr: *mut Counter) -> u32 {
let mut _counter = unsafe { &mut *ptr };
_counter.decr()
}
#[no_mangle]
pub extern fn destroyCounter(ptr: *mut Counter) {
let _counter: Box<Counter> = unsafe{ transmute(ptr) };
// Drop
}
Этот код я честно утащил отсюда, правда он не заработал на новой версии компилятора, да и с FFI::Raw у него тоже любовь не сложилась, пришлось его немного поправить. Но ничего, нам, пользователям Mojolicious, не привыкать.
Что здесь происходит? Сначала создаются две структуры данных: Args и Counter. В целом они одинаковые, но для Counter мы создаем реализацию, которая добавляет набор методов к нашей структуре.
Затем мы объявляем публичную функцию createCounter, которая на вход принимает указатель на структуру данных Args. Из этой структуры мы создаем объект Counter, который упаковываем в контейнер Box::new, который будет доступен через указатель. Работа с transmute дело опасное, поэтому мы оборачиваем создание контейнера в unsafe и этим самым мы говорим компилятору, что мы сами позаботимся о проверке на безопасность всей этой конструкции (чего мы не делаем в данном случае).
После создания контейнера мы возвращаем из функции изменяемый указатель, с которым и будем дальше работать везде.
Теперь создадим файл counter.pl, который будет использовать нашу новую библиотеку:
#!/usr/bin/env perl
use v5.16;
use FFI::Raw;
my $shared = 'target/release/libembed.dylib';
my $packed = pack('LL', 42, 21);
my $arg = FFI::Raw::MemPtr->new_from_buf($packed, length $packed);
my $createCounter = FFI::Raw->new(
$shared, 'createCounter',
FFI::Raw::ptr, FFI::Raw::ptr
);
my $getCounterValue = FFI::Raw->new(
$shared, 'getCounterValue',
FFI::Raw::uint, FFI::Raw::ptr
);
my $incrementCounterBy = FFI::Raw->new(
$shared, 'incrementCounterBy',
FFI::Raw::uint, FFI::Raw::ptr
);
my $ptr = $createCounter->($arg);
my $val = $getCounterValue->($ptr);
say "Current value [$val]";
my $nval = $incrementCounterBy->($ptr);
say "New value [$nval]";
Здесь мы сначала привязываемся к функции createCounter и говорим, что будем передавать указатель и ждем на выходе указатель. Затем привязываемся к функции getCounterValue и говорим, что передаем ей указатель и ждем unsigned int. Для всех остальных функций должны быть созданы аналогичные записи, здесь я их описывать не стал.
Дальше мы вызываем createCounter, которому передаем собранную руками структуру Args и получаем обратно указатель на контейнер со счетчиком. Дальше мы используем этот указатель для изменения значений счетчика.
Соберем это все дело и запустим:
alpha6$ cargo build --release
alpha6$ perl ./main.pl
Current value [42]
New value [63]
В выводе видно, что мы получили текущее значение счетчика после создания, а потом изменили его значение.
Теперь мы умеем получать структру из Rust и передавать ее обратно в библиотеку. Однако есть проблема — если мы хотим использовать эту структуру внутри Perl кода, то нас ждет небольшое разочарование. У меня так и не получилось привести то, что возвращает Rust, во что-то пригодное для использования в Perl. Если кто-то знает, как это сделать, — напишите в комментариях, а?
Но структуры-то получать хочется! Что ж, пришло время добавить еще один слой абстракции — используем FFI::Platypus вместо FFI::Raw.
FFI::Platypus является намного более суровым инструментом и предоставляет кучу опций, позволяя не заморачиваться с низкоуровневой работой с данными в отличие от FFI::Raw.
Проверим, что у нас все работает как надо. Установим FFI::Platypus и FFI::Platypus::Lang::Rust, этот модуль позволяет использовать при объявлениях структур и функций структуры из Rust, а не запоминать маппинг между Rust и C.
Изменим lib.rs и создадим файл plat.pl с содержимым, указанным ниже.
src/lib.rs:
#[no_mangle]
pub extern fn hello_plat(arg: u32) {
println!("Hello PL {}", arg);
}
plat.pl:
#!/usr/bin/env perl
use v5.16;
use FFI::Platypus;
my $shared = 'target/release/libembed.dylib';
my $ffi = FFI::Platypus->new();
$ffi->lib($shared);
$ffi->lang('Rust');
$ffi->attach( hello_plat => ['u32'] => 'void' );
hello_plat(1);
Здесь мы создаем объект FFI::Platypus и указываем ему путь к библиотеке — $ffi->lib($shared), затем указываем, какой язык мы хотим использовать $ffi->lang('Rust'). После этого указания FFI сам подгрузит модуль FFI::Paltypus::Lang::<LangName>, и можно будет использовать фичи для конкретного языка (соответствующий модуль должен быть предварительно установлен).
Далее мы добавляем функцию из библиотеки в перловый код, конструкция $ffi->attach создает функцию с указанным именем, которая транслирует данные в/из соответстующую библиотечную функцию. Функция создается в пространстве имен модуля, так что надо следить, чтобы не было перекрытия функций из библиотеки и текущего пакета.
Собираем, запускаем:
alpha6$ cargo build --release
alpha6$ perl ./plat.pl
Hello PL 1
Все работает как и задумано.
Настало время получить желанную структуру из нашей библиотеки.
В очередной раз переделаем наш src/lib.rs:
use std::mem::transmute;
pub struct Args {
init: u32,
by: u32,
descr: String,
}
impl Args {
pub fn new(init: u32, by: u32, descr: String) -> Args {
Args{
init: init,
by: by,
descr: descr,
}
}
}
#[no_mangle]
pub extern fn get_struct() -> *mut Args {
let my_struct = unsafe { transmute(Box::new(Args::new(42, 1, "hi from Rust!".to_string()))) };
my_struct
}
Здесь мы добавляем к нашей старой доброй Args еще одно поле с типом String, в которое мы будет записывать строку.
Затем мы создадим имплементацию для более каноничного создания нашей структуры (без этого можно обойтись, но неправильно это как-то). Создадим метод new, на вход которого будем передавать два числа и строку. Ну а дальше в функции просто создадим указатель на нашу структуру и вернем его перловому коду.
Обратите внимание, как создается объект String, в Rust есть два типа строк — str и String. В чем же отличия между str и String? str это так называемый строковый срез. Он имеет фиксированную длину и неизменяем, поэтому мы не можем использовать их в структуре (на самом деле можем, но с некоторыми затейливыми телодвижениями). Тип String представляет собой строку, размещаемую в куче и гарантированно явлющуюся UTF-8 строкой. Обычно String получается из str путем преобразования через .to_string.
Обратное преобразование осуществляется через & — str = &String.
Более подробно об этом можно почитать в документации.
Теперь получим данные из Rust:
use v5.16;
package My::RustStr;
use FFI::Platypus::Record;
# Описываем структуру данных, которую мы ждем из библиотеки
record_layout(qw(
uint rs_counter
uint rs_by
string rs_descr
));
my $shared = 'target/release/libembed.dylib';
my $ffi = FFI::Platypus->new;
$ffi->lib($shared);
$ffi->lang('Rust');
# Связываем структуру с нашим пакетом MyRustStr и присваиваем ей алиас rstruct
$ffi->type("record(My::RustStr)" => 'rstruct');
# Аттачим библиотечную функцию get_struct в наш пакет
# и указываем, что ждем от нее объявленную выше структуру
$ffi->attach( get_struct => [] => 'rstruct', sub {
my($inner, $class) = @_;
$inner->();
});
package main;
# Используем наш пакет My::RustStr для работы с библиотекой
my $str = My::RustStr->get_struct;
printf "Struct is [%d][%d][%s]\n",
$str->rs_counter,
$str->rs_by,
$str->rs_descr;
Сначала мы объявляем пакет My::RustStr, в котором описываем структуру данных, которую мы хотим от нашей библиотеки. Из-за особенностей модуля FFI запись record_layout может быть только одна на пакет (на самом деле нет, но лучше так не делать), поэтому мы выделяем нашу функцию в отдельный пакет.
Затем мы создаем новый тип данных в пакете My::RustStr $ffi->type("record(My::RustStr)" => 'rstruct'); и называем его rstruct.
Далее мы, как обычно, создаем биндинг до библиотеки и эспортируем ее в пространство имен пакета My::RustStr. Обратите внимание на $ffi->attach( get_struct => [] => 'rstruct', sub {, здесь в качестве возвращаемого значения функции мы указываем, что хотим получить нашу структуру rstruct. Также мы указываем кастомную функцию-обработчик, передавая coderef последним аргументом. В данном случае он не делает ничего кроме вызова библиотечной функции, но туда можно добавить какую-то дополнительную обработку до/после вызова внешней библитеки.
Затем мы просто распечатываем содержимое нашей структуры.
Теперь мы также можем передать структуру в нашу библиотеку без шаманства с pack и указателями. Изменим наш перловый код работы со счетчиком.
use v5.16;
package MyCounter;
use FFI::Platypus::Record;
record_layout(qw(
uint rs_counter
uint rs_by
));
package main;
my $shared = 'target/release/libembed.dylib';
my $ffi = FFI::Platypus->new;
$ffi->lib($shared);
$ffi->lang('Rust');
# Создаем новую структуру данных с именем CounterStr
$ffi->type('record(MyCounter)' => 'CounterStr');
# Аттачим функции из библиотеки
$ffi->attach(createCounter => ['CounterStr'] => 'opaque');
$ffi->attach(getCounterValue => ['opaque'] => 'u32');
$ffi->attach(incrementCounterBy => ['opaque'] => 'u32');
# Создаем структуру, из которой мы создадим счетчик
my $counter = MyCounter->new(
rs_counter => 10,
rs_by => 2,
);
# Создаем объект счетчика и используем его для работы
my $ptr = createCounter($counter);
say getCounterValue($ptr);
say incrementCounterBy($ptr);
Здесь мы используем opaque в качестве возвращаемого значения функции createCounter так как нам нужен сырой указатель для работы с остальными подключенными функциями. Но при желании этот указатель можно превратить в структуру нужного формата с помощью функции cast.
В Rust-коде ничего менять не надо.
Запускаем:
alpha6$ perl ./plat.pl
10
12
Итого, мы создали новый объект счетчика и изменили его.
Теперь мы умеем работать с большей частью случаев, которые нам могут пригодиться при использовании подключаемых библиотек (не только на Rust). Осталось научиться все это автоматически собирать при установке перлового модуля.
Для этого нам пригодятся Module::Build и Module::Build::FFI::Rust.
Приведем структуру проекта к виду:
Build.PL
ffi
lib
plat.pl
В lib у нас лежат Perl-библиотеки нашего проекта, в ffi — Rust-исходники. Build.PL выглядит так:
#!/usr/bin/env/perl
use strict;
use Module::Build;
use Module::Build::FFI::Rust;
my $build = Module::Build::FFI::Rust->new(
module_name => 'MyCounter::Record',
dist_abstract => 'FFI test',
create_makefile_pl => 'traditional',
dist_author => 'Denis Fedoseev <denis.fedoseev@gmail.com>',
create_readme => '0',
license => 'perl',
configure_requires => {
'Module::Build' => 0.38,
'File::Which' => 0,
'Module::Build::FFI' => '0.18'
},
build_requires => {},
requires => {
'perl' => '5.016000',
'FFI::Platypus' => 0,
'FFI::Platypus::Lang::Rust' => 0,
},
recommends => {},
recursive_test_files => 0,
sign => 0,
create_readme => 1,
create_license => 1,
);
$build->create_build_script;
Обратите внимание на использование Module::Build::FFI::Rust вместо обычного Module::Build. Запускаем установку, подготавливаем Build-скрипт:
alpha6$ perl ./Build.PL
Can't find dist packages without a MANIFEST file
Run 'Build manifest' to generate one
WARNING: Possible missing or corrupt 'MANIFEST' file.
Nothing to enter for 'provides' field in metafile.
Created MYMETA.yml and MYMETA.json
Creating new 'Build' script for 'MyCounter-Record' version 'v0.0.2'
Собираем наши модули:
alpha6$ ./Build
cargo build --release
Updating registry `https://github.com/rust-lang/crates.io-index`
Compiling winapi v0.2.5
Compiling libc v0.2.4
Compiling winapi-build v0.1.1
Compiling advapi32-sys v0.1.2
Compiling rand v0.3.12
Compiling embed v0.1.0 (file:///Users/alpha6/projects/tests/build/ffi)
Building MyCounter-Record
Устанавливаем:
alpha6$ ./Build install
cargo build --release
Building MyCounter-Record
Files found in blib/arch: installing files in blib/lib into architecture dependent library tree
Installing /Users/alpha6/perl5/perlbrew/perls/perl-5.20.3/lib/site_perl/5.20.3/darwin-2level/auto/MyCounter/Record/Record.bundle
Installing /Users/alpha6/perl5/perlbrew/perls/perl-5.20.3/lib/site_perl/5.20.3/darwin-2level/MyCounter/Record.pm
MyCounter/Record/Record.bundle это и есть наша Rust-библиотека, установленная в систему.
Теперь мы умеем работать с Rust-кодом из Perl и, самое главное, собирать из этого полноценные пакеты, которые и на CPAN выложить можно :)
В целом все, что описано в этой статье со стороны Perl, применимо к любому языку, который умеет изображать из себя C-библиотеку. Так что теперь вы всегда сможете взять нужную библиотеку, хоть на Java, и использовать в корыстных целях.
← Perl 6-винегрет | Содержание | Обзор CPAN за ноябрь 2015 г. →
