Выпуск 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-библиотеки нашего проекта, в ffiRust-исходники. 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 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье