Всем привет!

С недавнего времени я начал изучать прекрасный язык Rust. Практическое применение этого языка для себя я вижу во встраивании в критические по производительности места кода (по крайней мере, до момента «дозревания» и обрастания библиотеками и фреймворками).

Для закрепления теоретических навыков я решил сделать небольшой проект, суть которого состоит в следующем: динамическая библиотека на Rust реализует упрощенный вариант Алгоритма шинглов и посредством FFI позволяет подключать её (библиотеку). Всем кому интересно прошу под кат.

Для начала немного теории — что такое FFI? Вот мой вольный перевод по материалу с англоязычной википедии:
FFI (Foreign function interface) — механизм, позволяющий исполнять код, написанный на одном языке, другим (языком). Термин пришел из языка программирования Lisp (точнее его диалекта Common Lisp), также он именуется в Haskell и Python. В Java его называют JNI (Java Native Interface) или JNA(Java Native Access).

В большинстве случаев, FFI объявляется в языках высокого уровня (python, ruby, js), так чтобы была возможность использовать сервис, объявленный и реализованный на языке низкого уровня (C, C++). Это позволяет использовать одному языку программированию API OS, в котором они не определены (но определены в другом языке программирования) или улучшить производительность.

Основная функция FFI — это связывание семантики и соглашения о вызове одного языка (хоста) с семантикой и соглашением о вызове другого(гостя). Этот процесс должен учитывать среду исполнения и/или двоичный интерфейс обоих.

Так хватит теории, приступим к практике.

Rust
Для начало нужно создать новый проект при помощи команды (перед этим нужно установить Rust на вашу систему):
cargo new [название проекта]

После этого появится папка с названием, которое вы указали, со следующим содержимым


В конфигурационный файл Cargo.toml пишем следующий код:
Cargo.toml
[package]
name = "libshingles"
version = "0.1.0"
authors = ["Andrey Ivanov <lekkion@gmail.com>"]

[dependencies]
regex = "0.1.41"
rust-crypto = "^0.2"
libc = "0.1"

[lib]
name = "libshingles"
crate_type = ["dylib"]


В блоке [package] указываем информацию о вашем пакеты, в [dependencies] — подключаемые контейнеры, а в [lib] содержится указание скомпилировать библиотеку в виде стандартной динамической библиотеки (по умолчанию, компилирует в специфичный формат rlib).

Теперь непосредственно код lib.rs:
lib.rs
extern crate libc;
extern crate regex;
extern crate crypto;

use std::ffi::CStr;

use std::thread;
use regex::Regex;
use std::cmp::min;
use crypto::md5::Md5;
use crypto::digest::Digest;

#[no_mangle]
pub extern "C" fn count_same(text1: *const libc::c_char, text2: *const libc::c_char, length: libc::uint16_t) -> f32 {
	let buf1 = unsafe { CStr::from_ptr(text1).to_bytes() };
	let text1_ = match String::from_utf8(buf1.to_vec()) {
		Ok(val) => val,
		Err(e) => String::new(),
	};

	let buf2 = unsafe { CStr::from_ptr(text2).to_bytes() };
	let text2_ = match String::from_utf8(buf2.to_vec()) {
		Ok(val) => val,
		Err(e) => String::new(),
	};

	fn canonize(text: String) -> String {
		let html = Regex::new(r"<[^>]*>|[:punct:]").unwrap();
		let stop_words = Regex::new(r"(?i)\b[а-я]{1,2}\b").unwrap();
		let mut temp = html.replace_all(&text, " ");
		temp = stop_words.replace_all(&temp, " ");
		temp
	}

	fn get_shingles(text: String, len: u16) -> Vec<String> {
		let text = canonize(text);
		let split: Vec<&str> = text.split_whitespace().collect();
		let length = len as usize;

		if(split.len()<length) {
			return Vec::new();
		}

		let mut str: Vec<String> = Vec::new();
		for i in 0..(split.len()-length+1) {
			let mut buf = String::new();
			for y in i..i+length {
				buf = buf + " " + split[y];
			}

			let el = String::from(buf.trim()).to_lowercase();
			str.push(el);
		}

		let mut handles: Vec<_> = Vec::with_capacity(str.len());
		for item in str {
			handles.push(
				thread::spawn(move || {
					let bytes: &[u8] = item.as_bytes();
					let mut hash = Md5::new();
					hash.input(bytes);
					hash.result_str()
				})
			)
		}

		let mut res: Vec<String> = Vec::new();
		for h in handles {
			match h.join() {
				Ok(r) => res.push(r),
				Err(err) => println!("error {:?}", err),
			};
		}
		res
	}

	let shingles1 = get_shingles(text1_, length);
	let shingles2 = get_shingles(text2_, length);
	if(shingles1.len()==0 || shingles2.len()==0) {
		return 0 as f32;
	}

	let mut same = 0;
	for item in &shingles1 {
		for el in &shingles2 {
			if(*item == *el) {
				same += 1;
			}
		}
	}

	same = same*100;
	let length_text = min(shingles1.len(), shingles2.len());
	let length_text_f = length_text as f32;
	let same_f = same as f32;

	let result: f32 = same_f/length_text_f;
	result
}


Я уже упоминал, что алгоритм шинглов во многом упрощен: этап канонизации сведен к очистке от знаков препинания и удалению слов длиной менее два символа (такие слова обычно лишены всякой смысловой нагрузки). По-хорошему следовало очистить текст от прилагательных и отбросить от слов окончания, оставив только корни. Но для решения моих задач эта реализация подходит более чем.

В коде библиотеки я обращу внимание только на моментах относящихся к реализации FFI.
extern crate libc;  

Назначение этого контейнера в обеспечении всех определений, необходимых для взаимодействия с C-кодом на каждой платформе, поддерживающую Rust: определение типов, постоянных, а также заголовков функций.

Атрибут
#[no_mangle]
позволяет отключить дефолтовое поведение, которое изменяет имена функций.

pub extern "C"
указывает, что эта функция придерживается соглашения о вызове за пределами этого модуля по бинарному интерфейсу С.

text1: *const libc::c_char
text2: *const libc::c_char
Это типы сигнатур для CString

let buf1 = unsafe { CStr::from_ptr(text1).to_bytes() };
let text1_ = match String::from_utf8(buf1.to_vec()) {
	Ok(val) => val,
	Err(e) => String::new(),
};

let buf2 = unsafe { CStr::from_ptr(text2).to_bytes() };
let text2_ = match String::from_utf8(buf2.to_vec()) {
	Ok(val) => val,
	Err(e) => String::new(),
};
Здесь мы обозначаем как небезопасный код извлечение байтов по сырому указателю на CString. И конвертируем эти байты в тип String.

Вот и все. Важное замечание — не паникуйте при написании динамической библиотеки с использованием FFI! Использование макроса panic! приводит к неопределенному поведению. Если вы используйте panic!, необходимо выносить его в другой поток, так чтобы паника не всплывала к С.
Пример
use std::thread;

#[no_mangle]
pub extern fn oh_no() -> i32 {
    let h = thread::spawn(|| {
        panic!("Oops!");
    });

    match h.join() {
        Ok(_) => 1,
        Err(_) => 0,
    }
}


Компилируем библиотеку командой
cargo build

Cкомпилированная библиотека будет расположена по пути
/target/debug/
с расширением .so. Приступим к подключению нашей библиотеки в других языках.

Node.js
Чтобы подключить нашу динамическую библиотеку на этой платформе используем библиотеку, именуемую в npm как «ffi».
Вот пример подключения библиотеки:
index.js
var FFI = require('ffi');

var lib = FFI.Library('./target/debug/liblibshingles.so', {
    'count_same': [ 'float', [ 'string', 'string', 'int' ] ]
});

module.exports = lib.count_same;


var text1 = "Сегодня после аппаратного совещания, где был заслушан доклад министра энергетики, жилищно-коммунального хозяйства и государственного регулирования тарифов Удмуртии Ивана Маринина о подготовке к  следующему отопительному сезону, глава республики Александр Соловьев прокомментировал ситуацию с пуском тепла в этом году, сообщает пресс-служба главы и правительства Удмуртии. «У меня есть информация, как Ижевск готовился к отопительному сезону в 2010-м, в 2011-м и других годах, в том числе и нынче. Меня такая работа не устраивает, — сказал Александр Соловьев. — Много вопросов к Министерству жилищно-коммунального хозяйства, есть вопросы к заместителю председателя правительства Сивцову, который отвечает за ЖКХ. Это их работа — пусть разбираются». По словам руководителя региона, долги, из-за которых в том числе в Ижевске были проблемы с пуском тепла, влияют не только на отопительный сезон. Александр Соловьев сказал, что, имея большие долги, невозможно решать вопросы в Москве ни по дальнейшей газификации республики, ни по строительству плавательного бассейна. «Мне всегда говорят, что так не только у нас, но и в других субъектах. Не надо равняться на плохие примеры, — отметил глава Удмуртии. — Руководство городов обновили. Узкие места нам известны. Теперь при принятии бюджетов городов, бюджета республики необходимо учесть все ошибки, чтобы следующий отопительный сезон открыть безболезненно и своевременно. От каждого ответственного за тот или иной участок мне нужна эффективная работа — только и всего».";

var text2 = "Отвечая на вопрос представителей СМИ об использовании десяти новых автомобилей «Лада Веста», переданных в субботу республике Ижевским автозаводом, руководитель Удмуртии отметил, что эти машины получат районы республики, сообщает пресс-служба главы и правительства республики. Распределение произведено с учётом достижений той или иной территории и состояния используемых ныне автомобилей. «У некоторых глав уже совсем старые машины. Если бы было 25 автомобилей, я бы всем отдал. В бюджете у нас на приобретение транспорта было заложено 50 миллионов рублей, но я принял решение нынче не закупать новые машины», — сказал Александр Соловьев. «Лады Весты» будут переданы в распоряжение Алнашского, Балезинского, Игринского, Киясовского, Красногорского, Малопургинского, Селтинского, Сюмсинского, Юкаменского и Ярского районов.";

lib.count_same.async(text1, text2, 2, function(err, res) {
    if(err) {
        return console.log('err', err);
    }
    console.log(res)
});


Пакет ffi позволяет исполнять подключаемый код в другом потоке, используя libuv — для сохранения концепции асинхронности кода. Более подробная информация здесь.

Материалы, использованные при написании статьи:
RUST FFI Embedding Rust in projects for safe, concurrent, and fast code anywhere
Node FFI Tutorial
Foreign function interface
Foreign Function Interface Rust book

Также спасибо товарищу mkpankov

Код на гитхабе

Комментарии (16)


  1. k12th
    16.11.2015 17:41

    Хочется бенчмарков. Понятно, что должно быть быстрее, но ведь вызов через FFI тоже не бесплатный.


    1. stalehard
      16.11.2015 17:53

      Я думал над этим, но на Rust использован многопоточный вариант для самой тяжелой операции — вычисление хеша. Написать аналог под Node.js конечно можно, но там только на поднятие дочернего процесса уходит 30ms и 10 mb памяти.


      1. k12th
        16.11.2015 18:13

        Просто мотивация в таком случае размывается. Вот сидит человек и думает — купить мне еще серверов под ноду или переписать боттлнек на расте — как ему решить, что выгоднее экономически?


        1. mx2000
          16.11.2015 21:02
          +1

          при игре в долгую раст выигрывает в любом случае.


        1. Dreyk
          17.11.2015 00:27
          +1

          смотря что у вас есть лишнее: деньги или программисты


          1. Blumfontein
            17.11.2015 10:55

            Если есть программисты, то деньги тоже как бы есть :)


          1. k12th
            17.11.2015 11:29

            Вот надо понять, сколько надо денег, а сколько программистов. Может, у меня 100500 лишних пыхеров, от которых толку все равно не будет.


  1. Googolplex
    16.11.2015 18:04
    +1

    Я бы не советовал использовать usize в сигнатуре FFI-метода. В C нет непосредственного аналога usize. Лучше в этом случае взять lib::size_t, или вообще фиксированный тип.


  1. Mgrin
    16.11.2015 19:51

    По работе тоже нужно было ускорить критическую часть, тоже решил сделать это на Rust, заодно его и подучить. Но была одна маленькая деталь — функция в Rust должна была возвращать в Node.js объект, а не примитивный тип, и с этим я не справился. Велосипед в лице передачи строки и последующего JSON.parse, кроме потери любого выигрыша в производительности, не подошел т.к. возвращаемая строка была слишком длинной и все падало в момент получения нодой. Где-то прочитал, что в FFI можно передать указатель на объект, и в ноде его уже считать из памяти, но так и не разобрался с этим решением. Может у кого-нибудь была похожая задача?


    1. mkpankov
      16.11.2015 19:53
      +4

      На днях видел. Внизу есть пример с Node.js.

      Это вообще классный ресурс — там много примеров с разными типами данных и разными языками, рекомендую.


      1. Mgrin
        16.11.2015 19:57

        Спасибо огромное! Буду пробовать. А то у меня network analysis на графе в миллионы узлов — бедный Node.js считает несколько минут, пользователи наслаждаются логотипом загрузки…


      1. stalehard
        16.11.2015 22:49

        Ну или в крайнем случае использовать какую-ту прослойку в виде Redis-а



  1. grossws
    17.11.2015 13:06
    +1

    Важное замечание — не паникуйте при написании динамической библиотеки с использованием FFI!
    let text1_ = String::from_utf8(buf1.to_vec()).unwrap();
    Ну да =)

    Посмотрел в std::ffi, на CStr::from_ptr. Печально, что оно работает только с strlen, без возможности использовать strnlen. Т. е. словить segfault довольно легко. Понятно, что это unsafe, и словить segfault в нём вполне реально.


    1. stalehard
      17.11.2015 13:34

      Верно подметили