/usr/bin/ld: cannot find -liconv
on go-sdl2 static build
デスクトップ環境なし(X11 なし)の RaspberryPi OS Lite 入りラズパイで、スクリーン描画したい。
SDL という X11 の代替となるマルチメディア用のライブラリにスクリーンの描画機能があるらしい。ところが Go 言語(以下 Golang)で SDL を利用するために go-sdl2 を使うも、静的リンクでビルドすると「lib-iconv が見つかりません」とエラーが出るのです。
でもstatic
タグなし(go build .
)だと、ビルドできるし、ターミナルに描画はできるのです。どうしよう。
$ go build -v -tags static -ldflags "-s -w" .
github.com/veandco/go-sdl2/sdl
# github.com/veandco/go-sdl2/sdl
/usr/bin/ld: -liconv が見つかりません
collect2: error: ld returned 1 exit status
$ # 静的リンクしないなら大丈夫
$ go build -v .
$ echo $?
0
TL; DR (今北産業)
-
ビルド時に
rpi
タグ(-tags rpi
を付けます)- go build -v -tags static -ldflags "-s -w" . + go build -v -tags static -tags rpi -ldflags "-s -w" .
-
RPi OS (
buster
) はlibc6
にlib-iconv
をすでに持っています。しかしgo-sdl2
は静的リンク時に Gentoo Linux を想定して.a
ファイルが構成されているため、別途ラズパイ用のリンクファイルが用意されています。Golang のビルド時にrpi
タグを指定するとgcc
はラズパイ用のライブラリをリンクします。 -
関連 Issue
- Issue #394 static build in raspberry | go-sdl2 @ github.com
TS; DR (Raspberry Pi Zero + Raspberry Pi OS Lite + Go + go-sdl2 を使う細かい備忘録)
- 1024x600@60Hz の 7 inch タッチパネル式 Portable Display
- CX070PI_70R_50P_CTP
- V5.0 D190824
- Amazon で 9 千円弱で購入した Swehoo という怪しい俺様ブランドのメーカー不明品ですが、どうやら Waveshare 社の商品のコピー品のようです。
- 7inch HDMI LCD | Wiki @ WaveShare
上記 7 インチのタッチパネル式ディスプレイに、ラズパイ Zero W をつなげて、Raspberry Pi OS Lite(GUIなし・デスクトップなし版)のターミナル画面(CUI 画面)上に画像を描画する際の備忘録です。
操作は mac から SSH 接続していますが、実行すると手元のターミナルでなく接続されているモニタのターミナルに描画されます。
SDL2 とは
SDL (Simple DirectMedia Layer) は、C 言語で書かれたクロスプラットフォームのマルチメディアライブラリである。グラフィックの描画やサウンドの再生などのAPIを提供する。
オーディオ、キーボード、マウス、ジョイスティック、そして OpenGL および Direct3D を経由したグラフィックスハードウェアへのローレベルなアクセスを提供するよう設計されている。Windows、macOS、Linux、iOS、Android を公式にサポートしている。
(SDL @ Wikipedia より)
つまり、デスクトップ含む X11 などの GUI 系ライブラリがない環境でも、画面に描画したりデバイスからの入力を取得することができるライブラリです。SDL2 を使うとターミナル Only の CUI 画面にも画像を出力できます。
Go-SDL2 とは
go-sdl2 は、C 言語で書かれた SDL2 の Go 言語によるラッパー(バインディング)です。
例えば C 言語版 SDL2 の、とある関数を使いたい場合は、該当する Go-SDL2 のラッパー関数を使うと Go 言語でもモジュールとして使うことができる、という塩梅です。
go-sdl2
の使い方のポイント
「○○○ がしたいなぁ〜」と思ったら、SDL2 の Wiki で C 言語の関数名を探し、該当する go-sdl2
の Go 関数を go-sdl2 のドキュメントから探して使うことで Go に実装できます。
モニターの画面解像度を取得するまでの調べ方と実装例
-
本家 SDL2 の Wiki ドキュメントを開きます。
- https://wiki.libsdl.org/APIByCategory カテゴリ別 @ 公式 Wiki
最初から
go-dsl2
のドキュメントで探してもいいのですが、go-dsl2
はバージョン違いで仕様が結構変わります。そのため、本家 SDL2 の API の仕様を確認してから、go-dsl2
の自分が使っているバージョンのドキュメントを探す方が経験的に失敗が少ない気がします。 -
使えそうな関数を探します。
-
大カテゴリを見ると
Video
のDisplay and Window Management
が一番近そうです。- Display and Window Management(ディスプレイやウィンドウの管理)
-
Functions
項目(関数名一覧)を見るとSDL_Get*
のGetter
関数がいくつかあります。今回は「取得」したいのでGet
系にマトが絞れそうです。また、ウィンドウのサイズではなく「画面」サイズなのでDisplay
を含むものに絞ります。すると以下の 3 つの関数が使えそうです。- SDL_GetCurrentDisplayMode ... Get information about the current display mode.
- SDL_GetDesktopDisplayMode ... Get information about the desktop's display mode.
- SDL_GetDisplayMode ... Get information about a specific display mode.
-
各々の関数の説明をみると
SDL_GetCurrentDisplayMode
が近いと思われます。デスクトップなし環境でモニタは 1 つだからです。- SDL_GetCurrentDisplayMode ... 現在のモニタの状態に関する情報を取得
- SDL_GetDesktopDisplayMode ... 現在のデスクトップのモニタの状態に関する情報を取得
- SDL_GetDisplayMode ... 指定したモニタの状態に関する情報を取得
-
API の仕様(関数の引数と戻り値の構文)を確認します。
Syntaxint SDL_GetCurrentDisplayMode( int displayIndex, SDL_DisplayMode * mode ); // 戻り値は int 型 // 第 1 引数は int 型。名は displayIndex。 // 第 2 引数は SDL_DisplayMode 型オブジェクトのポインタ。名は mode。
ReturnReturns 0 on success or a negative error code on failure; call SDL_GetError() for more information. // 成功すると 0 が返され、失敗すると -1 以下の数値が返えされる。 // SDL_GetError() を呼び出すと詳細なエラーが得られる。
FunctionParameters(引数の仕様)displayIndex ... the index of the display to query mode ... an SDL_DisplayMode structure filled in with the current display mode // displayIndex ... 問い合わせ先のモニタ番号 // mode ... 現在のモニタ情報を埋めるための SDL_DisplayMode 構造体(いわゆる参照渡しで情報を取得する)
-
第 2 引数の
SDL_DisplayMode
型の構造を確認する。この構造を持ったオブジェクトに、各々の値が代入される。DataFieldsUint32 ... format ... one of the SDL_PixelFormatEnum values; see Remarks for details int ... w ... width, in screen coordinates int ... h ... height, in screen coordinates int ... refresh_rate ... refresh rate (in Hz), or 0 for unspecified void* ... driverdata ... driver-specific data, initialize to 0 // format フィールド ... Uint32 型の SDL_PixelFormatEnum で定義された enum 値 // w フィールド ... int 型の画面の横の座標 = 画面幅 // h フィールド ... int 型の画面の縦の座標 = 画面の高さ // refresh_rate フィールド ... int 型のリフレッシュレート(単位:Hz)、未定義の場合は 0 // driverdata フィールド ... void 型のポインタ。モニタドライバ特有のデータで、0 で初期化されていること。
-
いよいよ該当する
go-sdl2
の関数を確認- GetCurrentDisplayMode | v0.4.10 | go-sdl2 @ pkg.go.dev
-
go-dsl2
が v0.4.10 なので該当するバージョンのドキュメントを確認します。
GetCurrentDisplayModefunc GetCurrentDisplayMode(displayIndex int) (mode DisplayMode, err error) // GetCurrentDisplayMode returns information about the current display mode. // (https://wiki.libsdl.org/SDL_GetCurrentDisplayMode) // GetCurrentDisplayMode は現在のモニタの状態の情報を返します。
本家と違い、参照渡しではなく
DisplayMode
型のオブジェクトが返されるようです。 -
戻り値の
DisplayMode
型の構造を確認する。- DisplayMode | v0.4.10 | go-sdl2 @ pkg.go.dev
type DisplayMode struct { Format uint32 // one of the PixelFormatEnum values (https://wiki.libsdl.org/SDL_PixelFormatEnum) W int32 // width, in screen coordinates H int32 // height, in screen coordinates RefreshRate int32 // refresh rate (in Hz), or 0 for unspecified DriverData unsafe.Pointer // driver-specific data, initialize to 0 }
以上により、戻り値の
w
とH
フィールドを参照すれば画面サイズが取得できそうです。 -
Go のサンプル
// import "github.com/veandco/go-sdl2/sdl" func main(){ mode, err := sdl.GetCurrentDisplayMode(0) // 最初のモニタ情報を取得 if err != nil { log.Fatal(err) } fmt.Println("Width:", mode.W) fmt.Println("Height:", mode.H) }
-
検証環境
$ # モデル(Raspberry Pi Zero W H)
$ more /proc/device-tree/model
Raspberry Pi Zero W Rev 1.1
$ # OS 情報(Raspberry Pi OS Lite)
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
$ # カーネル情報
$ uname -a
Linux rpi-keinos 5.10.76+ #1474 Thu Oct 28 13:37:16 BST 2021 armv6l GNU/Linux
$ # Go 情報
$ go version
go version go1.17.2 linux/arm
依存 apt
パッケージのインストール
sudo apt-get install --yes \
build-essential \
libfreeimage-dev \
libopenal-dev \
libpango1.0-dev \
libsndfile1-dev \
libudev-dev \
libasound2-dev \
libjpeg-dev \
libtiff5-dev \
libwebp-dev \
automake
SDL2 の C ライブラリ・ビルド(X11 なし)
apt-get install
経由の SDL2 パッケージは古く、ラズパイ Zero + X11 なしにも最適化されていないのでソースからビルドしました。
# SDL2 ライブラリ本体のビルド
mkdir ~/SDL2
cd ~/SDL2
wget https://www.libsdl.org/release/SDL2-2.0.16.tar.gz
tar xzf SDL2-2.0.16.tar.gz
cd SDL2-2.0.16
mkdir build && cd build
../configure --host=arm-raspberry-linux-gnueabihf --disable-pulseaudio \
--disable-esd --disable-video-wayland \
--disable-video-x11
make install
# IMAGE(SDL2 関連ライブラリのビルド)
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.5.tar.gz
tar xzf SDL2_image-2.0.5.tar.gz
cd SDL2_image-2.0.5 && mkdir build && cd build
../configure
sudo make install
# Mpeg(SDL2 関連ライブラリのビルド。Mixer のビルド前にビルドする必要がある)
cd ~/SDL2
wget https://www.libsdl.org/projects/smpeg/release/smpeg2-2.0.0.tar.gz
tar xzf smpeg2-2.0.0.tar.gz
cd smpeg2-2.0.0 && mkdir build && cd build
../configure
sudo make install
# Mixer(SDL2 関連ライブラリのビルド)
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.4.tar.gz
tar xzf SDL2_mixer-2.0.4.tar.gz
cd SDL2_mixer-2.0.4 && mkdir build && cd build
../configure
sudo make install
# TTF(SDL2 関連ライブラリのビルド)
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.15.tar.gz
tar xzf SDL2_ttf-2.0.15.tar.gz
cd SDL2_ttf-2.0.15 && mkdir build && cd build
sed -i -e 's/have_opengl=yes/have_opengl=no/' ../configure
../configure
sudo make install
Go で go-sdl2
使う際のポイント
-
go get
でモジュールをrequire
する際は@master
と指定する。- go get github.com/veandco/go-sdl2/sdl + go get github.com/veandco/go-sdl2/sdl@master
Golang サンプル・コード
module github.com/KEINOS/sample
go 1.17
require github.com/veandco/go-sdl2 v0.5.0-alpha.1.0.20211011035448-93229da6a705
package main
import (
"os"
"github.com/veandco/go-sdl2/img"
"github.com/veandco/go-sdl2/sdl"
)
const (
bmpImagePath = "./sample.bmp" // 適当な ビットマップ画像
pngImagePath = "./sample.png" // 適当な PNG 画像
)
func run() (err error) {
var window *sdl.Window
var surface *sdl.Surface
var bmpImage *sdl.Surface
var pngImage *sdl.Surface
if err = sdl.Init(sdl.INIT_VIDEO); err != nil {
return
}
defer sdl.Quit()
// Create a window for us to draw the images on
if window, err = sdl.CreateWindow("Loading images", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, 800, 600, sdl.WINDOW_SHOWN); err != nil {
return
}
defer window.Destroy()
if surface, err = window.GetSurface(); err != nil {
return
}
// Load a BMP image
if bmpImage, err = sdl.LoadBMP(bmpImagePath); err != nil {
return err
}
defer bmpImage.Free()
// Load a PNG image
if pngImage, err = img.Load(pngImagePath); err != nil {
return err
}
defer pngImage.Free()
// Hide Cursor
_,_ = sdl.ShowCursor(sdl.DISABLE)
// Draw the BMP image on the first half of the window
bmpImage.BlitScaled(nil, surface, &sdl.Rect{X: 0, Y: 0, W: 400, H: 400})
// Draw the PNG image on the first half of the window
pngImage.BlitScaled(nil, surface, &sdl.Rect{X: 400, Y: 0, W: 400, H: 400})
// Update the window surface with what we have drawn
window.UpdateSurface()
// Run infinite loop until user closes the window
running := true
for running {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch event.(type) {
case *sdl.QuitEvent:
running = false
}
}
sdl.Delay(16)
}
return
}
func main() {
if err := run(); err != nil {
os.Exit(1)
}
}
静的ビルド
$ ls
sample.bmp sample.png go.mod main.go
$ go mod tidy && go mod download
...
$ go build -v -tags static -tags rpi -ldflags "-s -w" .
...
$ ls
sample.bmp sample.png go.mod main.go sample
$ ./sample
// ctrl+c するまで画面に表示される