Здравствуйте,
Меня зовут Александр Певзнер, и я программирую на Си и Go. Go обычно ассоциируется с бакендом, микросервисами и вот этим вот всем. Но я использую его необычным образом: я пишу на нём системное ПО.
Почему я это делаю именно на Go? Этот язык привлекает меня своей простотой, лаконичностью, ясной семантикой, прекрасной документацией и великолепной стандартной библиотекой.
Одна из моих программ, ipp-usb, написанная на Go, входит во все дистрибутивы Linux и *BSD и делает возможным использование принтеров и сканеров, которые подключаются к USB и поддерживают IPP over USB протокол - т.е., примерно всех современных.
А еще я член OpenPrinting - небольшой, но очень плодотворной группы людей, которая ответственна за печать и, отчасти, сканирование на всех UNIX-like OS и за формирование индустриальных стандартов в этой области.
Это всё начиналось для меня, как хобби, но сейчас это - часть моей оплачиваемой работы.
В силу особенностей моей работы меня не очень интересуют такие вещи, как поддержка миллиона запросов в секунду и прочий high load (это не значит, что мои программы тормозные. Но никто не дёргает системный принтер миллион раз в секунду). Но зато приходится разбираться с некоторыми другими непростыми штуками.
Об одной из таких штук и пойдёт речь в этой статье.
Понадобился мне для одного проекта на Go встроенный скриптинг. Ну т.е., чтобы программа могла всосать в себя скрипт, который определяет некоторые аспекты её поведения.
Размышляя о том, на каком языке программа должна скриптоваться, в выбирал между JS, Lua и Python.
Однако, JS и Lua - слишком нишевые языки. JS ассоциируется у всех с вебом а Lua - с разработкой игр. Таким образом, выбор естественным образом пал на Python. Этот язык знают все, а я испытываю некоторую надежду, что скрипты для моей программы буду писать не только я. Хотя сам я, должен признаться, Python не знаю и не люблю :)
Таким образом, осталось только придумать, как встроить интерпретатор Python-а в программу на Go.
Немного технических деталей.
Существует несколько реализаций Python-а, и cpython - самая "классическая" из них. Когда говорят о стандартном Питоне, имеют ввиду именно ее.
Сам cpython существует в виде динамической библиотеки, и программа python, привычный нам интерпретатор, является лишь надстройкой над ней.
API libpython3 более-менее документированный, и все примеры встраивания интерпретатора Питона в другие программы основаны именно на нём.
Особенности моей реализации
Для нетерпеливых читателей и немного забегая вперёд, перечислю ключевые особенности моей реализации биндинга Питона к Go, которые не встречаются в других реализациях:
Используется системная инсталляция libpython3, и нет привязки к конкретной версии. Программа, собранная с моим биндингом, не сломается при обновлении Питона, установленного в системе, и не потребует пересборки.
Можно создать в одной программе (в одном процессе) несколько независимых экземпляров интерпретатора Питона
Жизненный цикл объектов Питона, попавших в Go-программу, управляется стандартным Go-ным сборщиком мусора. Нет нужды вручную управлять счетчиком ссылок.
Зачем изобретать велосипед?
Разумеется, на необъятных просторах GitHub-а можно найти готовые решения. Например, вот это - весьма впечатляющий проект, покрывающий примерно весь API, реализуемый libpython3 и позволяющий как встраивать Python в Go-программы, так и наоборот, писать на Go модули для Питона.
Существуют и другие проекты. Полный обзор здесь я давать не буду.
Однако посмотрев на те из ник, которые удалось за разумное время найти, я обнаружил следующее.
Линковка на libpython3.XX.so, а не libpython3.so
libpython3 приходит в забавном виде: он состоит из библиотеки libpython3.XX.so, которая содержит все экспортируемые символы и libpython3.so, которая не содержит никаких символов, но ссылается (ELF NEEDED) на libpython3.XX.so.
Все, конечно, линкуются на libpython3.XX.so. Потому, что это удобно и понятно.
Однако в таком подходе есть существенный недостаток: стоит обновиться версии Питона, как все программы, динамически слинкованные с libpython3.XX.so перестанут работать, поскольку имя библиотеки поменяется (XX - это номер версии).
Мне же очень бы хотелось этого избежать и использовать для линковки libpython3.so.
У Питона, между прочим, есть Stable C API, предназначенный, правда, больше для модулей, написанных на Си, чем для программ, которые встраивают интерпретатор. И именно с этой целью, чтобы установленные модули выдерживали обновление установленного в системе Питона. Этот API содержит лишь подмножество полного API, но жить, в целом, можно.
Несколько интерпретаторов в одной программе.
Питон (libpython3) позволяет создать в программе (внутри одного процесса) несколько условно-независимых экземпляров интерпретаторов.
В документации это называется Sub-interpreter support.
У этих интерпретаторов есть некоторые общие ресурсы. Например, открытые файловые дескрипторы - они вообще принадлежат процессу и, поэтому, общие для всех. Или Global Interpreter Lock - он у них тоже общий (вернее, можно создать sub-interpreter со своим собственным GIL, но эта функциональность выходит за рамки Stable API, так что пришлось обойтись).
По некоторым причинам мне хотелось бы иметь доступ к этой функциональности. К сожалению, имеющиеся биндинги cpython к Go так не умеют.
Сборка мусора.
Питон - это язык со сборкой мусора и Go - это язык со сборкой мусора. Однако API libpython3 сишный, и там предлагается использовать счетчики ссылок для управления временем жизни питоньих объектов, попавших в сишную программу.
Существующие биндинги cpython к Go это так и оставляют.
Согласитесь, это выглядит несколько странно и не очень удобно, свинтить вместе два языка со сборкой мусора и управлять ссылками на объекты вручную.
Хотелось бы все же сохранить присущие сборке мусора удобства и предоставить Go-ному сборщику мусора управлять попавшими в Go питоньими объектами.
С учётом всех этих проблем, было решено сделать свой собственный биндинг. У меня не было потребности экспортировать в Go весь API libpython3, в конечном итоге моя цель - это встроенный скриптинг, а не полноценная двухсторонняя интеграция двух языков. Поэтому проект не обещал быть совсем уж неподъемным.
Утешаясь этой мыслью, я приступил.
Ключевые моменты процесса
Я всё же пишу статью, а не книгу, поэтому я предполагаю, что мои читатели умеют читать код на Go и на Си и понимают, как работает CGo.
Поэтому я остановлюсь только на неочевидных моментах.
Линковка на libpython3.so
Как я уже говорил, мне хотелось бы избежать прямой линковки на libpython3.XX.so, с указанием конкретной версии, чтобы к этой конкретной версии не привязываться и сделать, тем самым, возможным обновление установленного в систему Питона без необходимости пересборки использующих его программ.
К сожалению, если указать линкеру libpython3.so вместо libpython3.XX.so, линкер ругается, что символ-то есть, но вот только приходит он из библиотеки, которая напрямую в командной строке линкера не указана:
/usr/bin/ld: /tmp/go-link-783747701/000003.o: undefined reference to symbol 'Py_InitializeEx'
/usr/bin/ld: /usr/lib64/libpython3.13.so.1.0: error adding symbols: DSO missing from command line
Хотя у линкера и есть опция, позволяющая ему таскать символы из библиотек, которые не указаны напрямую, а подтягиваются по зависимостям, однако директива #cgo
не позволяет такую опцию указать. А если указывать ее снаружи, в Makefile
, то стандартная для Go сборка через go build
перестанет работать, что делает процесс сборки более затейливым, чем хотелось бы.
К счастью, хоть библиотека libpython3.XX.so напрямую и не указывается, однако при загрузке программы она подтягивается и символы из нее попадают в глобальное пространство имён процесса, откуда их можно выдернуть функцией dlsym. Несколько упрощая, это выглядит так, и это удобнее делать на Си:
static __typeof__(Py_InitializeEx) *Py_InitializeEx_p;
Py_InitializeEx_p = dlsym(RTLD_DEFAULT, "Py_InitializeEx");
if (Py_InitializeEx_p == NULL) {
fatal_error("%s: %s", "Py_InitializeEx", dlerror());
}
Конечно, вручную дёргать символы несколько муторно, но к счастью, их, необходиных, не так уж и много, около сотни.
Инициализация, создание экземпляра интерпретатора и взаимодействие с ним
Сам по себе процесс инициализации libpython3 и создания экземпляра интерпретатора несложен и даже относительно документирован.
Сложности начинаются, когда вы используется суб-интерпретаторы в многопоточной программе. И вот это вот не документировано от слова "совсем".
У Питона весьма своеобразный способ обращения с потоками операционной системы.
Питон использует thread-local storage системных нитей, чтобы хранить в нём свой контекст. При этом контекст ссылается на интерпретатор, с которым в данный момент работает данный поток, а интерпретатор хранит список всех связанных с ним контекстов.
В libpython3 API контекст, связанный с нитью, называется PyThreadState, а состояние интерпретатора представлено структурой PyInterpreterState. При этом функция, создающая экземпляр интерпретатора, Py_NewInterpreter сразу же и привязывает его к текущей нити и возвращает PyThreadState, который ссылается на данный интерпретатор.
Если интерпретатор в программе один, видимых проблем это почти не создаёт: Питон отложит свою личинку в каждой нити, и каждый контекст будет ссылаться на один и тот же экземпляр интерпретатора. Хуже, когда интерпретаторов много: приходится переключать эти контексты вручную, и нигде толком не описано, как это делать. Ну и засорять потоки операционной системы питоньими запчастями тоже не очень-то хочется. Тем более, что в программе на Go этих потоков может быть много.
И вот, что пришлось сделать.
Для создания новых экземпляров интерпретаторов я выделил отдельную нить, обёрнутою в гороутинку (которая использует runtime.LockOSThread, чтобы застолбить системную нить за собой).
С этой гороутинкой я общаюсь через каналы: посылаю ей запрос на создание нового интерпретатора в виде канала, куда она отправляет ответ.
Кстати, в Питоне всегда есть один "главный" интерпретатор, а суб-интерпретаторы создаются, как дополнительные к нему. Так что можно сказать, что эта нить - личная нить главного питоньего интерпретатора (который сам по себе, впрочем, не используется).
Само по себе создание суб-интерпретатора выглядит следующим образом.
Сначала, надо проинициализировать libpython3:
Py_InitializeEx_p(0);
py_main_thread = PyEval_SaveThread_p();
Здесь мы вызываем Py_InitializeEx (через указатель, Py_InitializeEx_p). Нулевой аргумент означает, что мы не хотим, чтобы Питон брал управление сигналами на себя, ими занимается Go.
Побочным (и не очень-то документированным) эффектом вызова Py_InitializeEx является создание питоньего контекста в вызывающей нити. Нам этот контекст еще пригодится, поэтому мы сохраняем его вызовом PyEval_SaveThread (основноё предназначение этой фукнции - это отцепить текущую нить от Питона, чтобы она могла заняться чем-нибудь блокирующимся, например, вводом-выводом, не удерживая при этом Global Interpreter Lock. Обратная к ней функция - PyEval_RestoreThread. Хотя глядя на всю эту механику не изнутри интерпретатора а снаружи, казалось бы более логичным поменять имена этих функций местами :)).
Теперь создание очередного суб-интерпретатора осуществляется следующим кодом:
PyInterpreterState *py_new_interp (void) {
PyThreadState *tstate, *prev;
PyInterpreterState *interp;
// This stuff is very tricky.
//
// We first call PyEval_RestoreThread(py_main_thread), to obtain
// the global interpreter lock.
//
// Then Py_NewInterpreter() creates a new PyThreadState for
// us and attaches it to the newly created sub-interpreter.
//
// We don't need this thread state and don't want to leak
// its memory.
//
// So we use PyThreadState_Swap back to the py_main_thread
// and destroy the newly created PyThreadState.
//
// Finally we need to PyEval_SaveThread() to release the
// the global interpreter lock.
PyEval_RestoreThread_p(py_main_thread);
tstate = Py_NewInterpreter_p();
interp = PyThreadState_GetInterpreter_p(tstate);
PyThreadState_Clear_p(tstate);
PyThreadState_Swap_p(py_main_thread);
PyThreadState_Delete_p(tstate);
py_main_thread = PyEval_SaveThread_p();
return interp;
}
Обратите внимание на непростое жонглирование контекстами потоков. Сначала мы вынуждены переключиться на сохранённый ранее контекст основного потока, который владеет основным интерпретатором. Потом создаётся новый интерпретатор, к которому в нагрузку прилагается новый контекст потока, и текущий поток переключается на этот новый контекст.. Нам этот контекст не нужен, поэтому вызовом PyThreadState_Swap мы возвращаем вещи в зад, а новый контекст потока уничтожаем, оставив себе интерпретатор.
Теперь как идёт обращение к интерпретатору. Если для создания интерпретаторов мы выделили отдельную нитку, то обращения к ним могут происходить с произвольной нитки, которая досталась в данный момент той гороутинке, которая захотела пообщаться с Питоном.
Перед обращением к интерпретатору мы должны для этой нитки вручную создать PyThreadState, привязанный к тому интерпретатору, к которому пойдёт обращение:
// py_enter temporary attaches the calling thread to the
// Python interpreter.
//
// It must be called before any operations with the interpreter
// are performed and must be paired with the py_leave.
//
// The value it returns must be passed to the corresponding
// py_leave call.
PyThreadState *py_enter (PyInterpreterState *interp) {
PyThreadState *prev, *t = PyThreadState_New_p(interp);
prev = PyThreadState_Swap_p(t);
return prev;
}
И по окончании взаимодействия с интерпретатором надо вернуть всё взад и удалить временный PyThreadState:
// py_leave detaches the calling thread from the Python interpreter.
//
// Its parameter must be the value, previously returned by the
// corresponding py_enter call.
void py_leave (PyThreadState *prev) {
PyThreadState *t = PyThreadState_Get_p();
PyThreadState_Clear_p(t);
PyThreadState_Swap_p(prev);
PyThreadState_Delete_p(t);
}
Разумеется, если перед каждым обращением к интерпретатору надо сказать py_enter, а по завершении - py_leave, то чего-нибудь можно и забыть. Поэтому на гошной стороне я эформил это так:
// Python represents a Python interpreter.
// There are may be many interpreters within a single process.
// Each has its own namespace and isolated from others.
type Python struct {
interp pyInterp // Underlying *C.PyInterpreterState
}
// pyGate represents the locked (attached to the current thread
// and with the GIL acquired) state of the Python interpreter.
//
// It works as a call gate into the interpreter and implements
// all interpreter operations that require locking.
type pyGate struct {
prev *C.PyThreadState // Previous current thread state
}
// pyGateAcquire temporary attaches the calling thread to the
// Python interpreter.
//
// It returns the pyGate object, that must be released after
// use with the [pyGate.release] call.
func pyGateAcquire(interp pyInterp) pyGate {
runtime.LockOSThread()
prev := C.py_enter(interp)
return pyGate{prev}
}
// release detaches the calling thread from the Python interpreter.
func (gate pyGate) release() {
gate.lastError() // Reset pending error condition, if any
C.py_leave(gate.prev)
runtime.UnlockOSThread()
}
А все вызовы интерпретатора оформлены, как методы pyGate. Теперь чтобы обратиться к интерпретатору, мы делаем как-то так:
// Object represents a Python value.
//
// Objects lifetime is managed by the Go garbage collector.
// There is no need to explicitly release the Objects.
type Object struct {
py *Python // Interpreter that owns the Object
oid objid // Object ID of the underlying *C.PyObject
}
// IsCallable reports if Object is callable.
func (obj *Object) IsCallable() bool {
gate := obj.py.gate()
defer gate.release()
pyobj := obj.py.lookupObjID(gate, obj.oid)
return gate.callable(pyobj)
}
Зачем в Object-е хранится не прямая ссылка на PyObject, а её ID, я расскажу чуть позже.
Фактически, структура pyGate существует с единственной целью: чтобы компилятор не дал нам забыть, что перед обращением к интерпретатору надо позвать py_enter, а после - py_leave.
И, наконец, освобождение интерпретатора после использования, тут тоже на Си:
// py_interp_close closes the Python interpreter.
void py_interp_close (PyInterpreterState *interp) {
PyThreadState *prev = py_enter(interp);
PyInterpreterState_Clear_p(interp);
py_leave(prev);
PyInterpreterState_Delete_p(interp);
}
Нам нужен py_enter/py_leave, чтобы переключиться на интерпретатор и подчистить его вызвом PyInterpreterState_Clear, но освобождение пямати вызовом PyInterpreterState_Delete делается уже после py_leave, потому что так написано в документации.
Сборка мусора
Одна из заявленных целей этого проекта - сделать так, чтобы о жизненном цикле объектов автоматически заботился сборщик мусора. А не мы сами, вручную.
Приделать к Object-у финалайзер (runtime.SetFinalizer), который освобождал бы PyObject, дело несложное, но есть один нюанс.
Интерпретатор может быть удалён явно, и не хотелось бы, чтобы относящиеся к нему объекты приходили к нему после удаления интерпретатора, когда до них дойдут руки у сборщика мусора.
С другой стороны, если интерпретатор будет вести учёт объектов, сборщик мусора до них не дойдёт никогда, потому, что на них всегда будет ссылка от интерпретатора, и они никогда не станут мусором.
Поэтому тут пришлось пойти на некоторую хитрость.
В Object-е хранится не указатень на PyObject, а 64-битное число, уникальный (в пределах данного экземпляра интерпретатора) идентификатор объекта, который присваивается ему при каждом создании. Он берется от 64-битного счётчика, и я не заморачиваюсь с переполнением этого счетчика, а просто кручу по кругу. Потому, что 64 бит - это много, это 18446744073709551615, и если каждую микросекунду создавать по объекту, то этого счётчика хватит на 584942 лет (а если каждую наносекунду - то на 585 кет). А к тому времени я выйду на пенсию, и с меня взятки будут гладки.
В интерпретаторе (структуре Python, оборачивающей PyInterpreterState с гошной стороны) хранится map[uibt64]*C.PyObject, в которой лежит маппинг этих OID-ов на указатели на питоньи объекты. Соответственно, если интерпретатор закрывается, все питоньи (но не гошные!) объекты, хранящиеся в этой мапе уничножаютсям, а сама мапа опустошается. И если когда-то позже финализатор объекта сработает, он попробует достать ссылку на PyObject из мапы, у него это не получится, и он на этом успокоится.
Это пример как раз того случая, когда любая проблема может быть решена добавлением еще одного уровня косвенности.
Разумеется, это решение имеет цену: теперь доступ к питоньему объекту из гошного всегда осуществляется через мапу. Но на фоне общих накладных расходов, связанных с Питоном, не думаю, что это внесет существенние замедление в работу программы (да и цели у меня не было, поставить рекорды быстродействия).
Обмен данными между Питоном и Go
Система типов Питона и Go очень разная. Причем у Питона она куда, как более замысловатая. Например там, где Go хватает одних только слайсов, в Питоне есть и списки (list) и тупли (tuple) и слайсы есть тоже. Питоньи целые числа (int) обычно ложатся на сишный int/long, но могут быть и с неограниченным количеством знаков, что соответствует гошному big.Int. Байтовые слайсы в Go - это просто []byte, а в Питоне для них выделено два отдельных типа: bytes (read-only) и bytearray (writable).
В общем, разная у них система типов, и надо как-то преобразовывать значения.
В направлении от Go к Питону всё получилось прям в виде полного автомата:
// NewObject creates a new Python Object for the Go value.
//
// The following Go types are supported:
//
// Go Python
// == ======
//
// nil None
//
// bool and derivatives PyBool_Type
//
// int, int8, int16, int32, PyLong_Type
// int64, uint, uint8, uint16,
// uint32, uint64 and derivatives
//
// string and derivatives PyUnicode_Type
//
// [*big.Int] PyLong_Type
//
// *Object new reference to the same PyObject
//
// []byte, [...]byte PyBytes_Type
//
// []any, [...]any PyList_Type
//
// [cmp.Ordered or bool]any PyDict_Type
func (py *Python) NewObject(val any) (*Object, error)
Вероятно, я добавлю по мере надобности способ намекнуть, что слайс иногда надо преобразовывать не в list, а в tuple. Но в целом, меня получившийся вариант устраивает.
Вызов Питоней функции из Go тоже выглядит вполне лаконочно:
// Call calls Object as function.
//
// Arguments are automatically converted from Go to Python.
// See [Python.NewObject] for details.
//
// Use [Object.CallKW] for call with keyword arguments.
func (obj *Object) Call(args ...any) (*Object, error)
Т.е., если obj - это callable Object, то позвать его можно вот просто вот так:
res, err := obj.Call(1, 2, 3)
Вариант вызова с именованными аргументами тоже предусмотрен:
// CallKW calls Object as function with keyword arguments defined
// by the kw parameter and positional arguments defined by the
// args parameter (variadic).
//
// Arguments are automatically converted from Go to Python.
// See [Python.NewObject] for details.
//
// If keyword arguments are not used, kw may be nil.
//
// It returns the function's return value.
func (obj *Object) CallKW(kw map[string]any, args ...any) (*Object, error)
А вот в обратную сторону, от Питона к Go, удобного и симметричного автоматического решения не получилось. Слишком уж затейлива система типов у Питона. Во что я должен превращать [1, 2, 3, 4, 5]
, в слайс целых чисел или в слайс объектов? Должен ли я распаковывать словари в map-ы? А если словать большой? А если очень большой? А если связанный с Object-ом map поредактировали, как map на гошной стороне, должно ли обновиться питонье представление объекта?
В общем, возникает много неудобных вопросов, для которых у меня нет красивого ответа.
В итоге я остановился на варианте, когда от Питона мы всегда получаем Object, и для него предусмотрены методы-аксессоры:
// Bool returns Object value as bool or an error.
func (obj *Object) Bool() (bool, error)
// Int returns Object value as int64 number or an error.
func (obj *Object) Int() (int64, error)
// Slice returns Object value as []*Object slice or an error.
// It works with sequence objects (lists, tuples, ...).
func (obj *Object) Slice() ([]*Object, error)
Можно ли посмотреть на код?
Можно.
Сам код: https://github.com/OpenPrinting/go-mfp/tree/master/cpython
Документация: https://github.com/OpenPrinting/go-mfp/tree/master/cpython
Можно смотреть и использовать. Лицензия - BSD, либеральнее не бывает. Гитхабовским звёздочкам и осмысленным PR-ам я тоже рад :)
Сейчас этот проект сыроват и будет активно изменяться, с учётом опыта использования там, для чего он изначально предназначался. Вероятно, когда всё более-менее устаканится, я вынесу его в отдельный репозиторий.
Но сейчас, что имеем, то имеем.
Вот.
Комментарии (6)
pnmv
18.06.2025 21:05Открывая статью, тщетно надеялся я узнать, для чего это всё. Не почему нужен свой вариант, ведь тут достаточно наличия личного интереса (любопытства) и большого количества свободного времени. Просмотрев статью, я не смог ответить на вопрос, для чего, вообще, иметь внутри хорошей, красивой программы на go, или на си++, такой якорь на шее, как какой-нибудь скриптовый язык?
apevzner Автор
18.06.2025 21:05Просмотрев статью, я не смог ответить на вопрос, для чего, вообще, иметь внутри хорошей, красивой программы на go, или на си++, такой якорь на шее, как какой-нибудь скриптовый язык?
Чтобы предоставить пользователю domain-specific language, который не надо отдельно учить.
pnmv
18.06.2025 21:05базовый синтаксис, конечно, практически везде тривиален, а как поглубже захочется, сиди, разбирайся, со всяким-разным.
Jijiki
18.06.2025 21:05примеры
откройте блендер и пройдите в консоль и попутно откройте документацию
скриптовые аналоги на движках (какие-то движки с ноукодинг там где открываем вьюшку с формами заполняем и оно компилируется)
функционал
рефлексия, моды, удобные конфигурации пример lua + C/C++ (с/без rttr)
RTTI можно открыть еще почитать (в дополнении почитать rttr для С++)
на просторах ютуба есть пример видео обзор движка сурс-лайк там показывается как это всё вместе работает(ну как пример потомучто реализации могут быть разные в частном случае)
синергия
если смотреть от синергии могут быть вопросы как это конкретно надо реализовывать, тут надо смотреть/тестировать - удобство и прочее
Octagon77
Это хорошо, что выбор пал на Python, хоть и противоестественным образом - читать интереснее, но и плохо тоже - выбор кажется мне необоснованным точно, и неудачным с нарушением принципа «по себе людей не судят».
JS никак не нишевый язык, на нём пишется всё кроме системного софта - и фронтенд, и бэкенд, и мобилки, и десктоп. И JS много быстрее Python.
Lua тоже не нишевый язык, на ней Neovim (с которым Lua ассоциируется раньше чем с играми), Roblox и куча всего работает, кроме того, она тоже несколько быстрее Python и надо бы посмотреть на сколько распухает приложение при каждом выборе.
У Lua, JS и Python было ровно одно преимущество - способность обходить W^X политику на мобилках. И один огромный недостаток - отлаживать, причём через задницу, без помощи компилятора (типа пользователь слышал, что если Rust скомпилировался, то работает).
Если мобилки не требуются, то естественный выбор - модуль plugin и Go. Я бы, как представитель пользователей, то есть почти покупатель который всегда прав, такому выбору порадовался.
Если мобилки требуются, то WebAssembly - она тоже обходит W^X политику и не навязывает конкретный язык. Этому выбору я, который всегда прав см. выше почему, тоже очень порадовался бы, а статья получилась бы и того интереснее.
apevzner Автор
Для меня Go был бы идеальным выбором. Я люблю этот язык. И есть интерпретатор, кстати.
Если выбирать под мой личный вкус из перечисленных, я бы предпочёл JS. Он хоть на Си похож. И опять же, есть интерпретатор, написанный на Go, и в отличии от Питона, никто не будет ждать от него полной совместимости (непонятно с чем, потому что реализаций JS в обороте много, и все разные).
Но что-то мне подсказывает, что встроенный Питон найдёт большее понимание среди широких народных масс. Хоть я и правда его не люблю.
Что до скорости, она в для меня не критична.