Выпуск 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 г. →