Перейти к содержанию

Rust + Node просто пример

Posted on:20 февраля 2023 г. at 12:02

Example Dynamic OG Image link

Привет вам, ребята! Я надеюсь, что у вас все в порядке, сегодня я попытаюсь объяснить, как мы можем интегрировать 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!

Я надеюсь, что этот пост будет полезен для вас!