Я работаю с Javascript в качестве своей повседневной работы уже несколько лет.
В последнее время я начал возиться с Rust, чтобы изучить новые концепции и понять, что такое шумиха (см. Опрос Stack Overflow 2023 )

Похоже, в последнее время все больше и больше JS-инструментов пишут на Rust:

Зачем кому-то это делать?

  • Javascript получил широкое распространение в отрасли, но производительность совсем невелика.
  • Rust обладает убийственной производительностью, но ему сложнее научиться

Объединение их обоих — это способ получить лучшее из обоих слов: разрабатывать на простом для изучения и широко распространенном языке, Javascript, и оптимизировать важные части с помощью Rust.

Итак, как можно объединить Rust и Javascript в кодовой базе? Давайте посмотрим на напи-рс!

npx @napi-rs/cli new

Проект по умолчанию определяет следующий файл src/lib.rs:

#![deny(clippy::all)]

#[macro_use] extern crate napi_derive;

#[napi] pub fn sum(a: i32, b: i32) -> i32 { a + b }

Запуск yarn build создаст:

  • Бинарный файл надстройки узла: <project name>.<target>.node
  • index.js, который определяет привязки JS для двоичного файла надстройки узла: он загружает правильный двоичный файл и экспортирует функцию sum.
  • index.d.ts, который предоставляет определения типов для index.js

Использование функции sum очень просто:

const { sum } = require("./index.js"); console.log(sum(40, 2));

Serde — это библиотека Rust, которая позволяет выполнять сериализацию и десериализацию из различных форматов данных, включая JSON.
Давайте сравним, насколько быстро мы можем работать с Serde + napi-rs по сравнению с парсингом JSON по умолчанию в JS.

Для этого теста мы будем анализировать объекты следующей формы:

{
  name: string
  phoneNumbers: string[]
}

Код Rust для функции parse на основе Serde выглядит следующим образом:

#![deny(clippy::all)]

#[macro_use] extern crate napi_derive;

use serde::{Deserialize, Serialize}; #[napi(constructor)]

#[derive(Serialize, Deserialize)]
pub struct Person {
  pub name: String,
  pub phones: Vec<String>,
}

#[napi]
pub fn parse(data: String) -> napi::Result<Person> {
  Ok(serde_json::from_str(&data)?)
}

Настройка
Чтобы этот код заработал, в Cargo.toml необходимо выполнить некоторые настройки как на serde, так и на napi:

Добавить serde-json в список функций для napi:

napi = { ..., features = ["napi4","serde-json",] }

Добавьте функцию derive в serde:

serde = { ..., features = ["derive"] }

Обработка ошибок

Rust и JS имеют очень разные системы обработки ошибок.

  • Исключения для JS
  • Тип результата для Rust

napi::Result<T> позволяет автоматически превратить Rust Error в исключение JS.
Запуск функции parse из файла JS с недопустимым JSON вызывает исключение:

const index = require("./index.js");

const parsedValue = index.parse("Invalid JSON");
console.log(parsedValue);
const parsedValue = index.parse("Invalid JSON");
                          ^

Error: expected value at line 1 column 1
    at ... { code: 'InvalidArg' }

parse также выдает исключение, если полученные данные имеют неправильную форму:

const index = require("./index.js");

const parsedValue = index.parse('{"name": "John Doe"}');
console.log(parsedValue);
const parsedValue = index.parse('{"name": "John Doe"}');
                          ^

Error: missing field `phones` at line 1 column 20
    at ... { code: 'InvalidArg' }

Быстрый микротест

Поскольку Serde выполняет синтаксический анализ И проверку, вот функция parse будет сравниваться с:

const yup = require("yup");

const schema = yup.object({
  name: yup.string(), 
  phones: yup.array(yup.string()),
});
const parseJs = (data) => {
  const d = JSON.parse(data);
  return schema.validateSync(d);
};

И эталонная функция:

const benchmark = (name, f, size) => {
  console.time(name);
  for (let i = 0; i < size; i++) {
    f(`{ "name": "John Doe", "phones": [ "+33 123456789" ] }`);
  }
  console.timeEnd(name);
};

На 1М звонках результаты следующие:

JS: 8.356s
Rust: 2.966s

Запуск тестов на 1000 итераций с объектами разного размера:

|`phones` array size|Rust (ms)|JS (ms)|
|-------------------|---------|-------|
|                  1|    4,758| 51,946|
|                 10|    9,334| 41,266|
|                100|   61,586|220,485|
|               1000|  499,942|   1987|
|              10000|     5338|  22685|

В этом тесте функция парсинга Rust работает в 4 раза быстрее, чем JS.

Заключение по этому эксперименту

Napi-rs позволяет легко использовать мощь Rust в кодовой базе Javascript:

  • Экспорт функций из Rust не требует особых усилий, и их можно легко импортировать в Javascript.
  • Похоже, что в этом есть выигрыш в производительности (хотя микротестам не следует уделять слишком много внимания).
  • Для этой настройки требуется этап сборки, что делает ее менее удобной, чем настройку только для JS.

Код, который я использовал для этого эксперимента, можно найти в этом репозитории github

Первоначально опубликовано на сайте thinkful-fiddler.dev 30 июля 2023 г.