> :!: Данная заметка расчитана на пользователей 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