Используем написанную на Rust динамическую библиотеку в Node.js

Разные подходы могут быть задействованы для решения этой задачи (динамические библиотеки, C++ аддоны Node.js и др).
В данной статье я хочу рассмотреть использование FFI [1] (Foreign Function Interface) и C-совместимой библиотеки написанной на Rust [2]. Этот способ не является самым эффективным по производительности, но тем не менее, может дать неплохой прирост с точки зрения оптимизации.
Приведенный здесь пример, однако, служит лишь для демонстрации того, как можно организовать взаимодействие Node.js и Rust, к самой описанной выше задаче он отношения не имеет.
Часть первая: Обмен строками
Задача: произвести обмен строками между Rust-библиотекой и Node.js, чтобы проиллюстрировать взаимодействие.
Часть вторая: Структуры
Задача: использовать структуры, для обмена данными и результатом между библиотекой и Node.js
В финальном примере я решил объеденить эти две части в одну.
Node.js
Напишем сценарий для Node.js, который обращается к динамической библиотеке и вызывает ее функции с передачей параметров и выводом возвращаемых значений.
В качестве зависимостей мы будем использовать:
- ffi-napi - Foreign function interface, расширение для загрузки и вызова динамических библиотек
- ref-napi - дополнительная зависимость, который поможет нам работать с некоторыми C-подобными типами в JavaScript
- ref-struct-napi - предлагает реализацию типа Struct для Node.js
Рассмотрим полный листинг index.js
const ffi = require('ffi-napi') | |
const ref = require('ref-napi') | |
const StructType = require('ref-struct-napi') | |
const ArrayType = require('ref-array-napi') | |
// Инициализиция C-подобного массива | |
const OutputArrayType = ArrayType(ref.types.int64, 2) | |
// Инициализиция C-подобной структуры | |
const OutputType = StructType({ | |
result: ref.types.int64, | |
operands: OutputArrayType, | |
description: ref.types.CString | |
}) | |
// Инициализиция C-подобной структуры | |
const OperationType = StructType({ | |
operand_a: ref.types.int64, | |
operand_b: ref.types.int64, | |
result: ref.types.int64, | |
}) | |
// Инициализиция динамической библиотеки | |
const lib = ffi.Library('target/release/lib_node_rust', { | |
hello: ['string', ['string']], | |
multiply: [OutputType, [OperationType]], | |
}) | |
// Вывод в консоль | |
process.stdout.write('\nCall the \'hello\' function:\n') | |
const stringToRust = 'Node.js' | |
// Вызов функции "hello" | |
let stringFromRust = lib.hello(stringToRust) | |
console.log(stringFromRust) | |
process.stdout.write('\nCall the \'sum\' function:\n') | |
// Создать С-подобную структуку с данными | |
const multiplication = new OperationType({ operand_a: 50, operand_b: -5 }) | |
// Вызов функции "multiply" | |
const result = lib.multiply(multiplication) | |
console.log(result.result) | |
console.log(result.operands.toArray()) | |
console.log(result.description) | |
// Print as an object | |
console.dir(result.toObject()) |
Несмотря на то, что код достаточно легко читаем, некоторые строки все же требуют пояснений.
*7 Для того, чтобы производить обмен данными с динамической библиотекой, мы должны вооружиться специальными типами, которые эта библиотека будет понимать. Мы не можем использовать JS массив и вместо этого мы будем работать с указателями (Pointer [3]). Для этого нам потребутся модуль (C++ addon) ref (который среди прочего позволяет создавать свои собственные типы), а так же модули реализующие типы структура и массив. В этой строке мы создаем C-подобный тип массива размером в 2 элемента, который будет содержать значения типа int64.
*10 Поскольку функция multiply библиотеки будет возвращать структуру (struct), представление которой в JavaScript отсутствует, нам потребуется модуль ref-struct, позволяющий работать с этим типом данных и, в частности, преобразовать его в объект JavaScript. В этом месте сценария мы создаем структуру OutputType, (подобно Rust или C) с тремя полями типов: int64,OutputArrayType,CString. Последний тип представляет собой так называемую Нуль-терминированную строку (NULL-terminated C-style strings). Такой строкой мы сможем пользоваться в библиотеке.
*24 Для инициализации доступа к функциям библиотеки мы используем вызов ffi.Library, который имеет следующую сигнатуру:
ffi.Library(путь_к_библиотеке, { имя_функции: [ возвращаемый_тип, [ тип_аргумента_1, тип_аргумента_2, ... ], ... ]);
Возвращаемый тип для функции multiply - созданная нами ранее структура OutputType. Аргументом функции является другая структура.
Вы могли заметить, что возвращаемый тип и тип аргумента функции hello есть простая строка string. Это то же самое что и ref.types.CString, никакой проблемы здесь нет, я сделал это нарочно, для демонстрации. Модуль ref позволяет вам использовать "string" и "number". Для подробностей, смотрите справку к модулю.
*35 Вызываем функцию hello библиотеки, как обычный метод объекта JavaScript.
*41 Создаем структуру с данными. Обратите внимание что свойство result я намеренно опускаю.
*46 Обращаемся к свойству структуры, так же как мы делаем с обычными объектами JS.
*47 Преобразовываем к обычному массиву.
*51 Поскольку мы не можем работать со структурами в Node.js, используя его нативные возможности, мы преобразуем ее в объект. (Заметьте, мы всегда можем обратиться напрямую к полям структуры, используя нотацию доступа к свойствам объектов JavaScript).
Rust
Теперь разберем полный листинг кода библиотеки
extern crate libc; | |
use libc::c_char; | |
use std::ffi::CString; | |
use std::ffi::CStr; | |
#[no_mangle] | |
#[allow(clippy::not_unsafe_ptr_arg_deref)] | |
// Возвращает входную строку объединенную со строковым литералом | |
pub extern "C" fn hello(input: *const c_char) -> *const c_char { | |
let input_cstring: &CStr = unsafe { | |
// Оборачивает необработанную C-строку в безопасную оболочку | |
// Функция является небезопасной | |
CStr::from_ptr(input) | |
}; | |
// Преобразует валидную UTF-8 CStr в строковый срез | |
let input_str: &str = input_cstring.to_str().unwrap(); | |
// Получает новую строку | |
let output_string: String = format!("Hello {} from Rust", input_str); | |
// Возвращает строку как c_char | |
CString::new(output_string).unwrap().into_raw() | |
} | |
#[repr(C)] | |
pub struct Output { | |
result: i64, | |
operands: [i64; 2], | |
description: *const c_char, | |
} | |
impl Output { | |
fn multiplication(operation: Operation) -> Output { | |
let description: String = format!("{} multiplied by {} is {}", operation.operand_a, operation.operand_b, operation.result); | |
Output { | |
result: operation.result, | |
operands: [operation.operand_a, operation.operand_b], | |
description: CString::new(description).unwrap().into_raw(), | |
} | |
} | |
} | |
#[repr(C)] | |
pub struct Operation { | |
operand_a: i64, | |
operand_b: i64, | |
result: i64, | |
} | |
impl Operation { | |
fn multiply(&mut self) { | |
self.result = self.operand_a * self.operand_b; | |
} | |
} | |
#[no_mangle] | |
// Выполняет умножение двух чисел и возвращает [`Output`] структуру | |
pub extern fn multiply(mut operation: Operation) -> Output { | |
operation.multiply(); | |
Output::multiplication(operation) | |
} |
Начнем с функции hello.
*1, *3-5 Для работы с С-типами нам потребуются некоторые зависимости.
*7, *56 Аннотация, указывающая компилятору Rust не искажать имя этой функции.
В процессе создания библиотеки Rust, в скомпилированном файле происходит изменение имени функции (техника называемая mangling [4]). Для того, чтобы функцию можно было вызвать из других языков, мы должны отключить такое поведение.
*8 Эта аннотация добавлена мной чтобы линтер clippy не вызывал ошибку о небезопасной функции.
*10 Функция объявляется как публичная - pub (может быть вызвана за пределами этого модуля), а ключевое слово extern обозначет что мы собираемся работать с FFI, то есть вызывать ее из другого языка. "C" говорит о том, какой двоичный интерфейс приложения (ABI) будет использоваться. ABI определяет, как вызывать функцию на уровне сборки. В данном случает это C.
Функция принимает и возвращает значения типа *const c_char
*const Т - тип "указатель" (raw pointer [5]). c_char - эквивалент типа char в языке С [6]
*14 Так как на вход функции мы получаем указатель, а работать мы хотим со строкой, то нам нужно выполнить небезопасный (по ряду причин, например из-за отсутствия гарантии валидности) метод from_ptr, который принимает указатель и возвращает представление заимствованной строки C ( &'a CStr). Эта структура позволяет нам использовать полезные методы. Поскольку функция небезопасная мы оборачиваем ее в блок unsafe *11
*17 Используем метод to_str() чтобы получить срез строки
*22 Возвращаем новую строку как указатель с типом c_char
Теперь рассмотрим функцию multiply и связанные с ней структуры
*25, *43 Аннотация #[repr(C)] перед описанием структуры Output, сообщает компилятору, что данный тип должен быть С-совместимым (с-representation) [7]
*29, *38 Текстовые данные мы возвращаем как тип указатель
Cargo.toml для библиотеки
Чтобы скомпилировать библиотеку нам нужно будет добавить следующие строки к Cargo.toml файлу:
[dependencies]
libc = "*"
[lib]
name = "_node_rust"
path = "src/lib.rs"
crate-type = ["dylib"]
Эти директивы говорят компилятору, что нам нужно получить динамическую библиотеку, здесь мы указываем путь к исходному файлу и название.
Теперь можно скомпилировать библиотеку cargo build --release и запустить node index.js
Как видно из примера мы можем довольно удобно использовать функции динамической библиотеки в Node.js, передавать в нее данные и возвращать результаты. Это может быть использованно для решения задачи, о которой я говорил в самом начале. Так что если вам нравиться JavaScript и Rust, то хорошо иметь ввиду такую возможность.
Но лучший способ понять принцип это его испробовать.
По этой ссылке вы найдте репозиторий описанного примера и инструкцию по установке. Удачи!
Ссылки:
[1]. Foreign Function Interface
[2]. Статья: Rust Inside Other Languages
[3]. Pointers in C programming language
[4]. Name mangling (Wikipedia)
[5]. Type: std/pointer
[6]. Type: std/c_char
[7]. Type layout: c-representation
Комментарии
Отправить комментарий