Здравствуйте,

Меня зовут Александр Певзнер, и я программирую на Си и 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)


  1. Octagon77
    18.06.2025 21:05

    Однако, JS и Lua - слишком нишевые языки. JS ассоциируется у всех с вебом а Lua - с разработкой игр. Таким образом, выбор естественным образом пал на Python

    Это хорошо, что выбор пал на Python, хоть и противоестественным образом - читать интереснее, но и плохо тоже - выбор кажется мне необоснованным точно, и неудачным с нарушением принципа «по себе людей не судят».

    JS никак не нишевый язык, на нём пишется всё кроме системного софта - и фронтенд, и бэкенд, и мобилки, и десктоп. И JS много быстрее Python.

    Lua тоже не нишевый язык, на ней Neovim (с которым Lua ассоциируется раньше чем с играми), Roblox и куча всего работает, кроме того, она тоже несколько быстрее Python и надо бы посмотреть на сколько распухает приложение при каждом выборе.

    У Lua, JS и Python было ровно одно преимущество - способность обходить W^X политику на мобилках. И один огромный недостаток - отлаживать, причём через задницу, без помощи компилятора (типа пользователь слышал, что если Rust скомпилировался, то работает).

    Если мобилки не требуются, то естественный выбор - модуль plugin и Go. Я бы, как представитель пользователей, то есть почти покупатель который всегда прав, такому выбору порадовался.

    Если мобилки требуются, то WebAssembly - она тоже обходит W^X политику и не навязывает конкретный язык. Этому выбору я, который всегда прав см. выше почему, тоже очень порадовался бы, а статья получилась бы и того интереснее.


    1. apevzner Автор
      18.06.2025 21:05

      Для меня Go был бы идеальным выбором. Я люблю этот язык. И есть интерпретатор, кстати.

      Если выбирать под мой личный вкус из перечисленных, я бы предпочёл JS. Он хоть на Си похож. И опять же, есть интерпретатор, написанный на Go, и в отличии от Питона, никто не будет ждать от него полной совместимости (непонятно с чем, потому что реализаций JS в обороте много, и все разные).

      Но что-то мне подсказывает, что встроенный Питон найдёт большее понимание среди широких народных масс. Хоть я и правда его не люблю.

      Что до скорости, она в для меня не критична.


  1. pnmv
    18.06.2025 21:05

    Открывая статью, тщетно надеялся я узнать, для чего это всё. Не почему нужен свой вариант, ведь тут достаточно наличия личного интереса (любопытства) и большого количества свободного времени. Просмотрев статью, я не смог ответить на вопрос, для чего, вообще, иметь внутри хорошей, красивой программы на go, или на си++, такой якорь на шее, как какой-нибудь скриптовый язык?


    1. apevzner Автор
      18.06.2025 21:05

      Просмотрев статью, я не смог ответить на вопрос, для чего, вообще, иметь внутри хорошей, красивой программы на go, или на си++, такой якорь на шее, как какой-нибудь скриптовый язык?

      Чтобы предоставить пользователю domain-specific language, который не надо отдельно учить.


      1. pnmv
        18.06.2025 21:05

        базовый синтаксис, конечно, практически везде тривиален, а как поглубже захочется, сиди, разбирайся, со всяким-разным.


    1. Jijiki
      18.06.2025 21:05

      примеры

      откройте блендер и пройдите в консоль и попутно откройте документацию

      скриптовые аналоги на движках (какие-то движки с ноукодинг там где открываем вьюшку с формами заполняем и оно компилируется)

      функционал

      рефлексия, моды, удобные конфигурации пример lua + C/C++ (с/без rttr)

      RTTI можно открыть еще почитать (в дополнении почитать rttr для С++)

      на просторах ютуба есть пример видео обзор движка сурс-лайк там показывается как это всё вместе работает(ну как пример потомучто реализации могут быть разные в частном случае)

      синергия
      если смотреть от синергии могут быть вопросы как это конкретно надо реализовывать, тут надо смотреть/тестировать - удобство и прочее