alchemyst/story/linux_go_blt_install_quickstart.md
2024-04-22 13:52:17 +03:00

154 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Установка и работа с BLT на Linux
==
Про Windows и Mac в контексте связки Go + BLT, я говорить не буду, поскольку не ел устриц.
С Linux, которая моя основная рабочая ось - другая история, здесь вносят свой шарм особенности работы линкера.
Дело в том, что BLT написана на C. Но! есть готовые биндинги для Go, НО! В Terminal/Include/Go по умолчанию
указаны такие флаги линкера CGO (стр 25)
```go
// #cgo LDFLAGS: -lBearLibTerminal
```
Что подразумевает глобальную видимость библиотеки. Увы, пока пакета с BLT для
распространенных дистрибутивов Linux нет.
Поэтому беде нужно помочь руками.
#####Первый метод
Сначала вручную показать линтеру, что такая библиотека есть, и потом перезагрузить
кеш путей к библиотекам (пример для Ubuntu):
```bash
$ sudo echo "/path/to/libbearterminal.so" > /etc/ld.so.conf.d/libbearterminal.conf && sudo ldconfig
```
Проблема тут в том, что эту же операцию придется проделать всем, кто захочет запустить ваше приложение с BLT. Вопреки
распространенному стереотипу - доля красноглазых пользователей Linux с каждым годом падает, и эта консольная магия для
большинства уже некомильфо.
#####Второй метод
Способ второй, которым воспользовался я, с которым намного проще жить.
Редактируем файл с биндингами (BearLibTerminal.go) примерно следующим образом:
```go
// #cgo LDFLAGS: -L. -Wl,-rpath -Wl,./ -lBearLibTerminal
// #include <stdlib.h>
// #include <BearLibTerminal.h>
import "C"
```
(знатоки С, простите если что не так, я этими флагами вообще
пользоваться не умею)
Далее - собираем минимальное приложение с blt ```go build -o test```.
Проверяем, что относительные пути записались в бинарник:
```bash
objdump -p test | grep RPATH
```
Результат должен быть таким:
```bash
RPATH ./
```
Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run
(или скомпилированный бинарник) прям оттуда. Собранные таким образом бинарники будут
искать библиотеку в той же папке, где находятся они сами.
Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом,
и все будет работать!
Горутины и многопоточность
---
Вторая кочка, на которой мне пришлось споткнуться - то то, что вызов любой своей
функции не из main thread BLT воспринимает крайне нервно,
и сыпет фатальными ошибками. Это неприятно, тк часто Го выбирают именно за
многопоточность из коробки. Но справедливости ради точно так же ведет себя и большинство
других библиотек связанных с рендером и вводом-выводом, тот же SDL например. Так что
воспринимайте это как милую особенность использования CGO.
Лекарство тут ровно одно - вызывать сишные foreign functions из main thread.
Для реализации этого требования мне показалась полезной следующая конструкция
```go
package main
import "runtime"
...
// Рецепт чтобы убежать от [fatal] 'refresh' was not called from the main thread
// https://github.com/golang/go/wiki/LockOSThread
func init() {
runtime.LockOSThread()
}
type GameState struct {
Mainfunc chan func()
}
// do запускает функцию f в контексте main thread.
func (g *GameState) Do(f func()) {
done := make(chan struct{}, 1)
g.Mainfunc <- func() {
f()
f = nil //zero pointer в замыкание
done <- struct{}{}
}
<-done
}
var State = GameState{
Mainfunc: make(chan func()), //блокирующий канал(!)
}
// И где-то в Main Loop делаем примерно так:
func MainLoop(state GameState) {
...
//В этом select обработка ввода, рендер, пеерколючение состояний интерфейса итп
for f := range state.Mainfunc {
f()
}}
...
```
State - это обычный Value Object, экземпляр типа GameState. Я его использую как
контейнер для важных для игры данных - географии уровня, состояния объектов и мобов,
разных тикеров, каналов для рендера и ввода-вывода итп[1]. Так как он глобальный
(или просто передается всюду по аргументам), то именно в него встроен метод Do.
Если нам скажем в пакете, где описывается некий предмет, надо нарисовать при его
поднятии какой-то супер-эффект на экране - мы поступаем вот так:
```go
package item
import "main"
import blt "some.repo.ru/user/bearlibterminal"
var State main.GameState
//функция скажем поднятия особенного предмета...
func (item *SpecialItem) Pickup() {
....
//выполняем строго в main thread
State.Do(func() {
renderSuperEffect()
})
}
...
//тут собственно отрисовка эффекта
func renderSuperEffect() {
...
blt.Layer(0)
blt.Print(x,y, "WAAAGH")
...
}
```
Здесь renderSuperEffect - непосредственная реализация эффекта, doSuperEffect -
запихивает в очередь на выполнения в main thread эту самую реализацию. Которая с успехом
выполняется в main loop. В целом картина именно такая, но больше подробностей можно
найти по ссылкам в комментариях.
[1]: Если такой контейнер аккуратно сериализовать (рекурсивно вместе со всем содержимым) и записать на диск... То потом можно его прочитать и десериализовать. Получив тем самым почти бесплатно Save / Load.