154 lines
7.4 KiB
Markdown
154 lines
7.4 KiB
Markdown
Установка и работа с 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.
|