> :!: Данная заметка расчитана на пользователей GNU/Linux. > FIXME Данной заметке не хватает ссылок на внешние источники. ====== Суть эксперимента ====== Python – это интерпретируемый язык динамической типизации и это имеет свои преимущества и минусы. Go, в свою очередь, – это компилируемый язык с статичной типизацией, то есть полная противоположность. Следовательно, объединение двух языков в одном проекте может иметь свои преимущества и самое банальное: оптимизация нагруженного участка кода, например, обращение к базе данных. Было предпринято решение попробовать объединить два языка через FFI((Foreign Function Interface - способ обмена данных между двумя языками.)) и предоставить экспериментальный экземпляр в совбатоке. **Обратим внимание, что так исторически сложилось, что FFI происходит через промежуточный этап прекомпиляции исходного кода в Си (C).** ====== Ход работы ====== Сделаем небольшие приготовления: mkdir experiment_ffi cd experiment_ffi go mod init exp/golang_ffi touch golib.go touch main.py touch build_ffi.py ===== Создание библиотеки на Go ===== Для начала необходимо написать рабочий код, который исполняется только на Go. Если бы мы писали веб сервер, то это был бы код обращения к базе данных, но мы пишем экспериментальный экземпляр – мы будем работать со строками. Например, мы разработаем библиотеку, которая здоровается с пользователем, а её ответ зависит от времени. А сам результат работы программы будет выводить на экран Python. Данная программа будет моделью для примера с веб-сервером. {{ :науки:информатика:model_explain.png?800 |}} Напишем часть на Go. package main import ( "time" ) func calcGreet() string { hour, _, _ := time.Now().Clock() var greet string switch { case hour <= 10 && hour > 4: greet = "Good morning, " case hour > 10 && hour < 18: greet = "Good afternoon, " case hour >= 18: greet = "Good evening, " case hour < 4: greet = "Good night, " default: greet = "Hello, " } return greet } func main() {} У нас есть код, написанный на Go, но он пока что ничего не делает, //потому что функция main пустая.// Далее мы будем видоизменять этот файл. В Go есть встроенные инструменты, упрощающие создание FFI, воспользуемся ими. Подключим пакет 'C' и напишем функцию, которая станет экспортируемым __интерфейсом__ для функции **calcGreet()**. // snippet... import "C" // snippet... //export hello func hello(name *C.char) *C.char { goname := C.GoString(name) // Конвертируем C строку в Go строку. greet := calcGreet() // Вызываем функцию, написанную на Go. result := greet + goname // Собираем результат. return C.CString(result) // Конвертируем результат в C строку и возвращаем его. } Обратим внимание, что инструкции, написанные на языке C внутри Go имеют схожий синтаксис с комментариями, **но у них есть отличие. Инструкции на C не могут иметь пробел после двух слешей.** Исходный код теперь имеет следующий вид: package main import "C" import ( "time" ) func calcGreet() string { hour, _, _ := time.Now().Clock() var greet string switch { case hour <= 10 && hour > 4: greet = "Good morning, " case hour > 10 && hour < 18: greet = "Good afternoon, " case hour >= 18: greet = "Good evening, " case hour < 4: greet = "Good night, " default: greet = "Hello, " } return greet } //export hello func hello(name *C.char) *C.char { goname := C.GoString(name) greet := calcGreet() result := greet + goname return C.CString(result) } func main() {} Теперь, чтобы собрать наш файл в библиотеку, вызовем компилятор и добавим к нему необходимые флаги. go build -buildmode=c-shared -o golib.so В рабочей директории автоматически соберутся два новых файла: * golib.so * golib.h Часть, написанная на Go готова и собрана. Теперь можно приступить к «склеиванию» проекта. ===== Склеивание проекта на Python ===== Python часто называют языком «клеем» и неспроста. Всё благодаря его скриптовой природе и большого разнообразия инструментов, заточенных под эту задачу. В нашем эксперименте мы воспользуемся встроенным модулем CFFI. Напишем файл, который описывает процесс сборки скомпилированных ранее файлов под Python. from cffi import FFI ffibuilder = FFI() # Укажите заголовочный файл, сгенерированный комплиятором Go, в качестве source. # Обратите внимание на синтаксис C! # Укажите shared object (.so файл), сгенерированный комплиятором Go, в качестве extra_objects ffibuilder.set_source( module_name="golib", source=""" #include "golib.h" """, extra_objects=["golib.so"], extra_link_args=["-Wl,-rpath=$ORIGIN"], ) # Скопируйте extern функции, которые описаны в Go, с конца заголовочного файла, чтобы Python автоматически сгенерировал обёртки для них. ffibuilder.cdef( csource=""" extern char* hello(char* name); """ ) if __name__ == "__main__": ffibuilder.compile(verbose=True) Таким образом, мы получим скрипт, который позволяет вызывать ранее описанную функцию //hello()//. Чтобы её вызывать воспользуемся стандартной командой: python build_ffi.py Если всё пройдет успешно, то вы увидете следующее: generating ./golib.c the current directory is '/home/dixi170/work/python/experiments' А в директории проекта появятся: * golib.c * golib.o * golib.cpython-{версия_питона}-{архитектура}-{ОС}.so Однако велика вероятность ошибки во время сборки. Это может быть либо опечатка в сборочном скрипте, либо отсутствие необходимых файлов. Чтобы исправить последнее, надо самостоятельно поискать информацию в интернете, как скачать //python3-devel// на ваш дистрибутив. Теперь у нас готова среда для работы с библиотекой на Go внутри Python. Допишем наш проект. from precompiled.golib import lib, ffi name = input().encode('utf-8') # Воспользуемся стандартной Python функцией //input()//. r = lib.hello(name) # Вызовем функцию, написанную на Go, которая сформирует строку приветствия с пользователем. print(ffi.string(r)) # Выведем результат. ====== Вывод ====== Мы успешно написали проект, который объединяет два совершенно разных языка программирования. Однако у этого подхода есть существенный недостаток: работа с памятью. В результате в нашей программе есть утечка: мы создаем строку в heap((34 строка файла golib.go)), которая не отслеживается ни Python, ни Go, и она будет висеть в памяти до конца жизни программы. Чтобы её убрать, нам необходимо пробросить функцию удаления из памяти через Go в Python. Из этого вытекает следующий минус: сложность. Мы обращаемся к низкоуровневому коду, когда работаем на высокоуровневых языках. Инструментария, который они предлагают, может быть недостаточно для такой работы. А незнание может породить //неопределённое поведение.// Также довольно легко передавать простые типы данных, но очень сложно передавать композитные (структуры в Go, классы в Python). Однако производительность, которую даёт этот способ, может затмить все опасности и минусы. ====== Приложение ====== Чтобы упростить процесс разработки, редактор dixi170 написал Makefile, который автоматизирует весь описанный процесс. help: @echo "Доступные комманды:" @echo "help - Вывод данного сообщения." @echo "clean - Очистка результатов сборки." @echo "build - Компиляция библиотеки на Go и сборка проекта." clean: rm -r precompiled/ build: go build -buildmode=c-shared -o golib.so python build_ffi.py mkdir precompiled mv golib.o precompiled/ mv golib.so precompiled/ mv golib.h precompiled/ mv golib.c precompiled/ mv golib.cpython* precompiled/ touch precompiled/__init__.py