[Из песочницы] Neon: Node + Rust

Предлагаю вашему вниманию перевод статьи «Neon: Node + Rust».

Javascript программистам, которых заинтриговала rust-овская тема бесстрашного программирования (сделать системное [низкоуровневое] программирование безопасным и прикольным), но при этом ждущих вдохновений или волшебных пендалей — их есть у меня! Я тут поработал немного над Neon — набором API и тулзов, которые делают реально легким процесс написания нативных расширений под Node на Rust.

TL; DR:


  • Neon — это API для создания быстрых, надежных нативных расширений Node на Rust
  • Neon позволяет использовать параллелизм Rust-а с гарантированной потокобезопасностью
  • Neon-cli позволяет легко и непринужденно создавать Neon проект и дает легкий старт… и наконец…
  • проекту требуется помощь!!!


Я научился готовить Rust, вы тоже научитесь


Я хотел сделать процесс настолько легким, насколько возможно (Ларри Уолл тоже с этого начинал, прим. переводчика) и написал для этого Neon-cli, консольную утилиту, которая в одну команду генерит шаблон Neon проекта, который собирается ничем иным как привычным npm install
Тут все очень просто. Для того что бы собрать наш первый модуль с Neon, ставим Neon-cli: npm install -g neon-cli, затем создаем, собираем и запускаем:

% neon new hello
...follow prompts...
% cd hello
% npm install
% node -e 'require("./")'


Для особо неверующих я тут выложил скринкаст, так что можете сходить и убедиться.

Ловлю тебя на слове [Take Thee at thy Word]


Чтобы продемонстрировать возможности Neon-а, я создал небольшое демо (считает количество слов). Демка простая — читаем полное собрание пьес Шекспира и считаем число вхождений слова «тебя» (I Take Thee at thy Word — цитата из Ромео и Джульетты) Сначала я попытался сделать это на ванильном javascript. Для начала мой код разбивает текст на строки и считает количество найденных вхождений для каждой строчки:

function search(corpus, search) {
  var ls = lines(corpus);
  var total = 0;
  for (var i = 0, n = ls.length; i < n; i++) {
    total += wcLine(ls[i], search);
  }
  return total;
}


Поиск в строке включает в себя разбиение на слова и сравнение каждого слова с искомым:

function wcLine(line, search) {
  var words = line.split(' ');
  var total = 0;
  for (var i = 0, n = words.length; i < n; i++) {
    if (matches(words[i], search)) {
      total++;
    }
  }
  return total;
}


Оставшие за кадром детали можно посмотреть в этом коде, он маленький и автономный (без зависимостей)
На моем ноуте код отрабатывает по всем пьесам Шекспира за 280–290ms. Не так уж и плохо, но как говорится, есть к чему стремиться.

И сельскому веселью предадимся [ Fall Into our Rustic Revelry ]


Одно из самых замечательных свойств Rust-а заключается в том что крайне эффективный код может быть удивительно компактным и читаемым. В Rust-овской версии код подсчета вхождений для строк выглядит почти так же как JS код:

let mut total = 0;
for word in line.split(' ') {
    if matches(word, search) {
        total += 1;
    }
}
total // в Rust можно опустить `return` при возврате значения


На самом деле такой код можно написать с более высокоуровневыми абстракциями без потери производительности используя итерационные (перебирающие) методы как filter и fold (аналоги Array.prototype.filter и Array.prototype.reduce в JS):

line.split(' ')
    .filter(|word| matches(word, search))
    .fold(0, |sum, _| sum + 1)


Мои эксперименты (на скорую руку) показали даже незначительный (на пару миллисекунд) прирост производительности. Мне кажется это прекрасная демонстрация Rust-овской парадигмы абстракций с нулевой стоимостью, где высокоуровневые абстракции дают в итоге сравнимый или даже превосходящий по производительности (за счет дополнительных возможностей для оптимизации, например отказ от проверок границ) код, чем низкоуровневый и более запутанный.
На моей машинке Rust-овская версия отрабатывает за 80–85ms. Неплохо, трехкратный рост только за счет использования Rust-a, причем примерно с таким же объемом кода (60 строк в JS, 70 — Rust). И кстати, я тут сильно округляю числа — это ведь не rocket scince, я всего лишь хочу показать что вы можете получить значительное повышение производительности используя Rust, но все зависит от ситуации.

И нить их жизни прядется [Their Thread of Life is Spun]


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

let total = vm::lock(buffer, |data| {
    let corpus = data.as_str().unwrap();
    let lines = lines(corpus);
    lines.into_iter()
         .map(|line| wc_line(line, search))
         .fold(0, |sum, line| sum + line)
});


vm::lock API дает Rust-тредам безопасный доступ к Node-объекту Buffer (то есть к строго типизованному массиву), блокируя при этом исполнение JS кода.

Что бы продемонстрировать, насколько это легко я использовал новый Rayon от Niko Matsakis — набор прекрасных абстракций для параллельной обработки данных. Изменения в коде минимальны — просто меняем цепочку into_iter/map/fold/ на это:

lines.into_par_iter()
     .map(|line| wc_line(line, search))
     .sum()


Обратите внимание — Rayon не разрабатывался специально для Neon, просто Rayon реализует протокол итераторов Rust, поэтому Neon может использовать его из коробки.
С этими небольшими изменениями мой двухядерный MacBook Air выполняет демку за 50ms вместо 85ms на предыдущей версии.

Bridge Most Valiantly, with Excellent Discipline


Я постарался сделать интеграцию настолько гладкой, насколько это возможно. Со стороны Rust, функции Neon следуют простому протоколу, получают Call объект и возвращают JavaScript значение:

fn search(call: Call) -> JS {
    let scope = call.scope;
    // ...
    Ok(Integer::new(scope, total))
}


Объект scope безопасно отслеживает хандлы (Handle) в V8 heap-е. Neon API использует систему типов Rust — это дает гарантию что ваш модуль не уронит приложение неправильным управлением Handles объектов (тут ковыряются в кишках Node, заодно и Handle используют, можно посмотреть… 2009-ый, сейчас на Хабре так уже не пишут…).

Со стороны JS загрузка модуля проста до безобразия:

var myNeonModule = require('neon-bridge').load();


Отчего этот шум? [Wherefore is this noise]


надеюсь этого демо будет достаточно что бы заинтересовать вас. Помимо фана, я думаю быстродействие и параллельность — сильные аргументы за использование Rust в Node. Так как экосистема Rust растет, это может стать неплохой возможностью получить доступ Node к либам Rust. Как следствие, я надеюсь Neon сможет стать хорошим уровнем абстракции который сделает процесс написания расширений для Node менее болезненным. С проектами вроде node-uwp может быть даже стоит исследовать развитие Neon в сторону уровня абстракции над JS-engine (что бы это ни значило).

В общем тут море возможностей, но… мне нужна помощь! Для тех кто хочет поучаствовать — я создал чатик в Slack для community, инвайт можно получить тут, а также IRC канал #neon на Mozilla IRC (irc.mozilla.org).

благодарности


Тут много в чем еще разбираться и тонны недоделанной работы, но и то что сделано было бы невозможно без помощи: Andrew Oppenlander«s blog post дал мне нащупать почву под ногами, Ben Noordhuis и Marcin Cieślak научили готовить V8, я утащил пару приемов из злодейски гениального кода написанного Nathan Rajlich; Adam Klein и Fedor Indutny помогли понять V8 API, Alex Crichton помог мне с таинством компиляции и линковки, Niko Matsakis помог с дизайном API безопасного управления памятью, а Yehuda Katz помог со всем остальным дизайном.

Если вы хоть что-то поняли из сказанного — возможно вы тоже можете помочь!

© Habrahabr.ru