В рамках одного из моих SDK-проектов нам понадобилось добавить скриптование, которое бы оказало наименьший эффект на размер конечного бинарного файла, но при этом предоставляло хорошую фунциональность. Это дало старт проекту, который описан в этой статье. Прошу заметить, что т.к. в SDK у нас есть определенные требования, они частично перенеслись на язык скриптования, поэтому в проекте не участвовали некоторые достаточно известные встраиваемые ЯП (но Lua включен для сравнения).


Сайт проекта доступен по ссылке. Скажу сразу, что на данный момент для меня лично победителями являются Chibi-Scheme и Wasm3. Подробности для заинтересовавшихся под катом.


Базовые требования были следующие:


  • Максимальная переносимость (только C/C++).
  • Достаточно строгая типизация (не позволяющая складывать апельсины с яблоками), соответственно реализации ECMAScript не рассматривались.
  • Лицензия, дружелюбная к коммерческим проектам.

Отдельным пунктом было требование к синтаксису ЯП по дружелюбности к транформации кода. Тут может возникнуть много вопросов, поэтому сразу уточню несколько моментов:


  • Можно трансформировать и байт-код (если он поддерживается), это могло бы убрать требования к изначальному синтаксису. Однако, в общем случае это тяжелее сделать, плюс формат байт-кода может быть проприетарным или нестабильным.
  • Обязательная трансформация, которая нам нужна, это очистка кода от отладочной и неиспользуемой функциональности. Это служит не только минификации, но и убирает некоторые векторы атак;
  • Необязательная, но желательная трансформация — минификация приватных символов, это нужно больше для обфускации и минификации;
  • Необязательная, но желательная трансформация — полное удаление переносов строк, служит больше для минимизации.

Первоначальный список учитывал только ЯП, которые поддерживают все требования (включая необязательные), но был расширен в соответствии с пожеланиями сообщества.


Изначальный список был взят с проекта, в котором они пытаются учесть все возможные встраиваемые ЯП: https://github.com/dbohdan/embedded-scripting-languages Возможно, список не полный, и не содержит дополнительные ЯП, которые могли бы удовлетворять вышеуказанным требованиям. Если вы знаете о таких, пожалуйста, дайте мне знать либо в личку, либо комментарием.


Тестовый код для всех ЯП включал в себя создание и вызов функции, которая складывает "Hello, " и результат вызова внешней (C/C++) функции "read", возвращающей "world". Код функции представлен в таблице как образец синтаксиса. Конечный исполняемый файл скомпилирован с оптимизацией по размеру и с удалением всех ненужных символов (GCC -s). Значения RSS снимались во время выполнения "read()".


Платформа: Linux amber 5.9.1-arch1-1 #1 SMP PREEMPT Sat, 17 Oct 2020 13:30:37 +0000 x86_64 GNU/Linux


ЯП Размер ELF (KiB) RSS (KiB) Пример скрипта
TinyScheme 84 3048 (define fn(lambda () (string-append "Hello, " (read))))
Wasm3 159 6024 N/A [1]
MicroPython 186 1968 import builtins; fn = lambda: 'Hello, ' + builtins.read()
Lua 247 1760 function fn() return "Hello, " .. read() end
Chibi-Scheme 259 3828 (define fn(lambda () (string-append "Hello, " (read))))
Squirrel 270 1808 function fn() { return "Hello, " + read(); }
ArkScript 443 1760 (let fn(fun() (+ "Hello, " (read))))
Gravity 500 3524 extern var read; func fn() { return "Hello, " + read(); }
Janet 525 4152 (defn fn[] (string "Hello, " (read)))
ChaiScript 1342 4912 def fn() { return "Hello, " + read(); }
AngelScript 1878 4716 string fn() { return 'Hello, ' + read(); }
Python 3543 9320 import usermod; fn = lambda: 'Hello, ' + usermod.read()

[1] Wasm3 работает с WebAssembly, поэтому пример кода привести нельзя (это будет бинарный дамп). Но приведу пример исходника Rust, который используется для генерации этого бинарного файла (_m3-функции используется для обмена данных с интерпретатором):


#[no_mangle]
pub fn r#fn() -> i32 {
    let read_str = load_m3(unsafe { read() });
    return store_m3(&format!("Hello, {}", read_str));
}

Описание и документация по этим ЯП приведены в GitLab-проекте, здесь же хочу поделиться своими впечатлениями:


  • TinyScheme реализует лишь подмножество R5RS, используется в GIMP. Если бы не было Chibi-Scheme с полноценной поддержкой R7RS, возможно стал бы победителем. Также немного смущает хостинг на SourceForge используя SVN репозиторий с одной только trunk-веткой.
  • ChaiScript очень интересный — наверное самый простой по удобству встраивания, плюс поддерживается крупными компаниями. К сожалению, тот факт, что он состоит только из заголовочных файлов, очень сильно замедляет компиляцию (мой Dell XPS 13 компилирует минимум 2-3 минуты). Также, он достаточно сильно увеличивает размер исполняемого файла.
  • Gravity может понравиться любителям Swift. Достаточно интересный ЯП, но к сожалению, не так много документации. Ориентирован на мобильную разработку.
  • Squirrel достаточно компактный, но достаточно сложно встраивать (любая ошибка в работе со стеком вызовов приводит к краху приложения — у них недостаточно хорошая система обработки ошибок).
  • MicroPython не очень дружелюбен к встраиванию, надо либо ориентироваться, что во время компиляции он сгенерирует main() для вашего проекта, либо писать достаточно большие Makefile-ы. Документация по встраиванию тоже весьма скудна.

Победителя два:


  • Wasm3, т.к. позволяет использовать разные ЯП для написания скриптов, и достаточно компактный. К сожалению, сам по себе формат WASM достаточно небогатый (например, нет поддержки строк и сборщика мусора), и хотя WASI улучшает ситуацию, многое приходится писать самим или полагаться на рантайм, добавляемый компиляторами. Также для тестового кода я реализовал небольшой менеджер памяти, чтобы обмениваться данными между песочницой и интерпретатором, ничего встроенного wasm3 пока не имеет.
  • Chibi-Scheme, т.к. поддерживает современный компактный Scheme (R7RS) и имеет достаточно большую дополнительную библиотеку (плюс стандартная библиотека Scheme и возможность использовать уже написанный код). Естественно, встраивание библиотек увеличит конечный код, но даже без библиотек он может полноценно работать (в тестовом коде я не встраивал init-7.scm полностью, а только добавил процедуру string-append).

Встраивание внешних процедур достаточно простое (хотя я вставил лямбду, Chibi-Scheme полноценно работает с чистым C):


res = sexp_define_foreign(*ctx, sexp_context_env(*ctx), "read", 0,
                          [](sexp ctx, sexp self, sexp_sint_t n) -> sexp {
                            return sexp_c_string(ctx, "world", -1);
                          });

Из интересных дополнительных модулей хотел бы отметить:



К сожалению, документация небогатая, но компенсируется доступом ко всему исходному коду. Плюс, Chibi-Scheme известен в своих кругах, и не будет потенциально брошен или переведен в режим минимальной поддержки, как некоторые другие потенциальные кандидаты.




Буду рад услышать все комментарии, и рассмотреть другие ЯП в рамках вышеупомянутых требований. Исследование до сих продолжается, и возможно победитель сменится, но это уже будет зависеть от вас.