> :!: Данная заметка расчитана на пользователей GNU/Linux.
> FIXME Данной заметке не хватает ссылок на внешние источники.

====== Суть эксперимента ======

Python – это интерпретируемый язык динамической типизации и это имеет свои преимущества и минусы. Go, в свою очередь, – это компилируемый язык с статичной типизацией, то есть полная противоположность. Следовательно, объединение двух языков в одном проекте может иметь свои преимущества и самое банальное: оптимизация нагруженного участка кода, например, обращение к базе данных. 

Было предпринято решение попробовать объединить два языка через FFI((Foreign Function Interface - способ обмена данных между двумя языками.)) и предоставить экспериментальный экземпляр в совбатоке.

**Обратим внимание, что так исторически сложилось, что FFI происходит через промежуточный этап прекомпиляции исходного кода в Си (C).**

====== Ход работы ======

Сделаем небольшие приготовления:
<code bash>
mkdir experiment_ffi
cd experiment_ffi
go mod init exp/golang_ffi
touch golib.go
touch main.py
touch build_ffi.py
</code>

===== Создание библиотеки на Go =====

Для начала необходимо написать рабочий код, который исполняется только на Go. Если бы мы писали веб сервер, то это был бы код обращения к базе данных, но мы пишем экспериментальный экземпляр – мы будем работать со строками.

Например, мы разработаем библиотеку, которая здоровается с пользователем, а её ответ зависит от времени. А сам результат работы программы будет выводить на экран Python. 

Данная программа будет моделью для примера с веб-сервером.
{{ :науки:информатика:model_explain.png?800 |}}



Напишем часть на Go.

<file go golib.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() {}
</file>

У нас есть код, написанный на Go, но он пока что ничего не делает, //потому что функция main пустая.// Далее мы будем видоизменять этот файл.

В Go есть встроенные инструменты, упрощающие создание FFI, воспользуемся ими.

Подключим пакет 'C' и напишем функцию, которая станет экспортируемым __интерфейсом__ для функции **calcGreet()**.
<code go>
// 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 строку и возвращаем его.
}
</code>

Обратим внимание, что инструкции, написанные на языке C внутри Go имеют схожий синтаксис с комментариями, **но у них есть отличие. Инструкции на C не могут иметь пробел после двух слешей.**

Исходный код теперь имеет следующий вид:

<file go golib.go>
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() {}

</file>


Теперь, чтобы собрать наш файл в библиотеку, вызовем компилятор и добавим к нему необходимые флаги.
<code bash>
go build -buildmode=c-shared -o golib.so 
</code>

В рабочей директории автоматически соберутся два новых файла:
  * golib.so
  * golib.h

Часть, написанная на Go готова и собрана. Теперь можно приступить к «склеиванию» проекта.

===== Склеивание проекта на Python =====

Python часто называют языком «клеем» и неспроста. Всё благодаря его скриптовой природе и большого разнообразия инструментов, заточенных под эту задачу. В нашем эксперименте мы воспользуемся встроенным модулем CFFI.

Напишем файл, который описывает процесс сборки скомпилированных ранее файлов под Python.
<file python build_ffi.py>
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)
</file>

Таким образом, мы получим скрипт, который позволяет вызывать ранее описанную функцию //hello()//. Чтобы её вызывать воспользуемся стандартной командой:

<code bash>
python build_ffi.py
</code>

Если всё пройдет успешно, то вы увидете следующее:
<code>
generating ./golib.c
the current directory is '/home/dixi170/work/python/experiments'
</code>

А в директории проекта появятся:
  * golib.c
  * golib.o
  * golib.cpython-{версия_питона}-{архитектура}-{ОС}.so

Однако велика вероятность ошибки во время сборки. Это может быть либо опечатка в сборочном скрипте, либо отсутствие необходимых файлов. Чтобы исправить последнее, надо самостоятельно поискать информацию в интернете, как скачать //python3-devel// на ваш дистрибутив.

Теперь у нас готова среда для работы с библиотекой на Go внутри Python. Допишем наш проект.

<file python main.py>
from precompiled.golib import lib, ffi

name = input().encode('utf-8') # Воспользуемся стандартной Python функцией //input()//.

r = lib.hello(name) # Вызовем функцию, написанную на Go, которая сформирует строку приветствия с пользователем.

print(ffi.string(r)) # Выведем результат.
</file>

====== Вывод ======

Мы успешно написали проект, который объединяет два совершенно разных языка программирования. Однако у этого подхода есть существенный недостаток: работа с памятью. В результате в нашей программе есть утечка: мы создаем строку в heap((34 строка файла golib.go)), которая не отслеживается ни Python, ни Go, и она будет висеть в памяти до конца жизни программы. Чтобы её убрать, нам необходимо пробросить функцию удаления из памяти через Go в Python. 

Из этого вытекает следующий минус: сложность.

Мы обращаемся к низкоуровневому коду, когда работаем на высокоуровневых языках. Инструментария, который они предлагают, может быть недостаточно для такой работы. А незнание может породить //неопределённое поведение.// Также довольно легко передавать простые типы данных, но очень сложно передавать композитные (структуры в Go, классы в Python).

Однако производительность, которую даёт этот способ, может затмить все опасности и минусы.



====== Приложение ======

Чтобы упростить процесс разработки, редактор dixi170 написал Makefile, который автоматизирует весь описанный процесс.

<file bash 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
</file>

