[Из песочницы] 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 помог со всем остальным дизайном.
Если вы хоть что-то поняли из сказанного — возможно вы тоже можете помочь!