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<Integer> {     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 помог со всем остальным дизайном.

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

ссылка на оригинал статьи https://habrahabr.ru/post/277453/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *