Привет вам, ребята! Я надеюсь, что у вас все в порядке, сегодня я попытаюсь объяснить, как мы можем интегрировать Rust с Nodejs, и я подумал, что этот пример будет хорошей идеей.
Если у вас нет знаний о Rust, не беспокойтесь, этот пост будет действительно прост для понимания, и я думаю, что он даст вам представление об этом фантастическом языке программирования.
Прежде всего, нам нужно создать наш проект, это будет минимальный проект, в данном случае мы собираемся создать alarm в качестве примера, вы должны будете установить rust и nodejs, прежде чем перейти к дальнейшему эксперименту.
Поехали!!!
Создаём новый проект
Мы собираемся создать нашу основу, внутри вашей пустой папки мы выполним команду:
cargo init --lib
Это создаcт ваш пустой проект rust.
После этого мы собираемся создать наш пустой проект nodejs с помощью этой команды:
npm init --y
Затем нам нужно установить пакет, который поможет нам создать наш rust-код и переместить библиотеку по определенному пути (в данном случае это будет корневой путь нашего проекта).
npm i cargo-cp-artifact
Применение конфигурации
Для обеспечения связи между нашим проектом nodejs
и нашей библиотекой rust
нам необходимо установить neon, этот модуль дает нам возможность создать привязку между нашими двумя проектами. Вы можете добавить в наш Cargo.toml
, которая выглядела бы следующим образом:
[package]
name = "my_alarm"
version = "0.1.0"
license = "ISC"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
[dependencies]
[dependencies.neon]
version = "0.10"
default-features = false
features = ["napi-6", "task-api", "channel-api"]
если вы хотите узнать больше о Cargo.toml
, я настоятельно рекомендую вам ознакомиться с форматом манифеста.
Этого было достаточно для нашей конфигурации rust, давайте перейдем к нашей конфигурации проекта nodejs, в наш package.json
мы можем добавить наш процесс сборки, для хорошей практики я собираюсь поместить его в раздел нашего сценария сборки:
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics"
}
Эта команда позволяет нам скомпилировать нашу библиотеку с помощью cargo build
и переместить двоичный файл в корневой каталог с именем index.node
.
Очень важно иметь одинаковое имя в обоих проектах, поэтому имя в нашем package.json будет:
"name": "my_alarm"
Добавление логики
Я собираюсь добавить весь код для нашего проекта rust, а затем я объясню, как это работает.
В наш src/lib.rs
мы можем добавить этот код:
use neon::prelude::*;
use std::{thread, time};
fn alarm(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let wait_time = cx.argument::<JsNumber>(0)?.value(&mut cx);
let callback = cx.argument::<JsFunction>(1)?.root(&mut cx);
let channel = cx.channel();
thread::spawn(move || {
let duration = time::Duration::from_secs(wait_time as u64);
thread::sleep(duration);
channel.send(move |mut cx| {
let callback = callback.into_inner(&mut cx);
let params_callback = vec![cx.null().upcast::<JsValue>()];
let callback_context = cx.undefined();
callback.call(&mut cx, callback_context, params_callback)?;
Ok(())
});
});
Ok(cx.undefined())
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("alarm", alarm)?;
Ok(())
}
и в нашем test.js
файл (вы можете назвать его как хотите):
const {alarm} = require('./index.node');
const time = 5;
alarm(time, () => {
console.log(I woke up after ${time} secs!);
clearInterval(myTick);
})
// Это просто для того, чтобы проверить, что мы не блокируем наш eventloop
const myTick = setInterval(() => {
console.log('I advanced 1 sec');
}, 1000)
Ладно! Теперь пришло время объяснить наш код.
Давайте посмотрим нашу основную функцию:
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("alarm", alarm)?;
Ok(())
}
В этой части мы объявляем наш модуль, если вы работаете с nodejs, а также с браузером, вы можете объявить несколько модулей (commonjs, esm и т.д.), Которые представляют собой изолированные блоки кода, которые мы можем импортировать для его использования. Что ж, как вы можете видеть, мы объявили этот блок кода:
cx.export_function("alarm", alarm)?;
Для простоты мы можем прочитать это как “Я объявляю метод alarm и экспортирую его, а позже мы сможем использовать его для любого модуля, который мы хотим импортировать в нашем коде nodejs.
Теперь давайте проверим наш код alarm-а:
fn alarm(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let wait_time = cx.argument::<JsNumber>(0)?.value(&mut cx);
let callback = cx.argument::<JsFunction>(1)?.root(&mut cx);
let channel = cx.channel();
thread::spawn(move || {
let duration = time::Duration::from_secs(wait_time as u64);
thread::sleep(duration);
channel.send(move |mut cx| {
let callback = callback.into_inner(&mut cx);
let params_callback = vec![cx.null().upcast::<JsValue>()];
let callback_context = cx.undefined();
callback.call(&mut cx, callback_context, params_callback)?;
Ok(())
});
});
Ok(cx.undefined())
}
В этом месте:
let wait_time = cx.argument::<JsNumber>(0)?.value(&mut cx);
let callback = cx.argument::<JsFunction>(1)?.root(&mut cx);
Мы получаем два первых параметра нашей функции, вы помните это? Я напомню:
alarm(time, () => {
console.log(I woke up after ${time} secs!);
clearInterval(myTick);
})
Как вы видите, у нас есть два параметра, и в нашем коде rust мы получаем эти значения, первое - числовое значение, а второе - функция (наш обратный вызов), также важно отметить, что у нас есть:
let channel = cx.channel();
Это важно, потому что дает нам возможность взаимодействовать с eventloop (по сути, основным потоком нашего приложения nodejs).
И здесь происходит волшебство:
thread::spawn(move || {
let duration = time::Duration::from_secs(wait_time as u64);
thread::sleep(duration);
channel.send(move |mut cx| {
let callback = callback.into_inner(&mut cx);
let params_callback = vec![cx.null().upcast::<JsValue>()];
let callback_context = cx.undefined();
callback.call(&mut cx, callback_context, params_callback)?;
Ok(())
});
});
Я уже говорил о eventloop раньше, не так ли? Как разработчик, вы должны знать, что мы не можем заблокировать наш eventloop из-за того, что мы создаем новый поток (это только для нашего примера, если вы хотите иметь несколько потоков, вы можете использовать rayon и т.д.). Итак, в нашей логике мы переводим поток в спящий режим на время, которое мы передали в нашем первом аргументе, а затем мы вызываем наш обратный вызов, в данном случае это будет простой обратный вызов с нулевым аргументом (значение ошибки).
Теперь пришло время протестировать наше решение!
Сначала нам нужно создать наш rust-код, мы можем сделать это с помощью команды:
npm run build
После этого мы собираемся протестировать наше приложение nodejs: node test.js
и бум!
I advanced 1 sec
I advanced 1 sec
I advanced 1 sec
I advanced 1 sec
I woke up after 5 secs!
I advanced 1 sec
Самая важная часть нашего вывода заключается в том, что мы не блокируем наш eventloop!
Я надеюсь, что этот пост будет полезен для вас!