Go言語の -buildmode=c-archive と Lazarus でのスタティックリンク(Windows/Linux)
前回の記事では Windows で
最初の一歩
前回の話を踏まえると、Lazarus(Free Pascal Compiler) で
もう少し詳しく見てみると、Windows では
「この問題を解決するためにどうするのがいいか」という以前にそもそもこの辺あまり詳しくないので、わかる範囲で少し調べながら対応方法を検討した。
その上で一番正しいっぽい道のりは多分「FPC 側でオブジェクトファイルの .init_array / .ctor セクションを参照して初期化処理を実行する」みたいな感じだと思うのだが、ちょっと話のスケールが大きい上に既存の振る舞いとの互換性の問題もありそうで手を出しにくいので、この方向は諦めた。
もし実現させるならプログラム固有のコードじゃなくてランタイム側のコードを変える話になりそう。
次に考えたのは「Go言語側でのビルド時に Windows では外部シンボルになっているんだから、挙動が違う原因を突き止めて両方外部シンボルにできれば解決するのでは?」という方向性。
しかし、そもそも外部シンボルになっているものを
最終的に選んだアプローチは「
Linux 側で
最初はGo言語のソースコードの src/runtime 辺りにファイルを追加して、全体を make し直して使えるようになることを確認していたのだが、もう少し調べてみると別に src/runtime に置く必要は全然なかったようなので、github.com/oov/rt0 という、コードが全然ないパッケージを作った。
darwin とか試してないものもあるが、取りあえず
これで、
初期化処理を正しく呼ぶ
Windows の時はパラメータを特に定義せずに呼んでいたのだが、同じように Linux で
Go 1.6.2 で同じことをしてもエラーメッセージが出ずに実行されたのだが、つい最近追加された部分だったらしい。
そこで rt0_linux_amd64.s のコードを読んでみると、tip の rt0_windows_amd64.s とは違い DI と SI には本来 argc と argv が入っているらしいことがわかった。呼び出し規約を確認してみると2つの引数が渡されてきた時の形なので「あ、これ要するに main 関数じゃん!」などとやっと気付いた勘の悪い自分であったが、結果的に
前回の記事で SIGFPE で止まっていた問題は Windows でもいつからか再現しなくなってしまったので、Linux 上でも起こるかどうかなどは謎。
処理をまとめる
これで Windows と Linux で同じ地点まで辿り着いたので、あとは同じソースコードで動くようにしたい。
まずはGo言語側。前回より少しメモリコピー回数がマシになっている。
OS によって処理が分かれている部分も特にないのであまり見どころはない。
あとは Windows の時はリンクするファイルが増えるのでそれも ifdef で囲っておく。
Windows では argc / argv を参照していないが、呼び出し規約的に動作に影響はないはずなので処理は分けてない。
入力もファイル名の代わりに TStream にできると思うけど、巨大なファイルをメモリの固まりじゃなくてストリームとして綺麗に渡すのは結構面倒な話になりそうなのでここでは触れないでおく。
使う側は
-buildmode=c-archive
を試した。今回はそれと同じことを Ubuntu 上でも行ってみて、最終的に同じソースコードで両方の環境でコンパイルできるところまで持ち込む。
最初の一歩
前回の話を踏まえると、Lazarus(Free Pascal Compiler) で
go build -buildmode=c-archive
でコンパイルしたバイナリをリンクしようとする時は、概ね以下のようにすると最低限処理が動くようになるはずだった。- FPC のリンカだと上手く動かないので Use external linker オプションを付ける必要がある
→ プロジェクトオプションのコンパイラオプション → Custom Options から -Xe を追加 - ライブラリのファイル名が lib*.a でなければ上手く認識させられない
→ Go言語側で main パッケージのディレクトリ名を libhoge という感じにしておく - 初期化処理が呼ばれないので自分で呼ぶ必要がある
→_rt0_amd64_linux_lib
を呼びたい(呼べなかった)
もう少し詳しく見てみると、Windows では
nm libhoge.a | grep _libなどとすると
_rt0_amd64_windows_lib
は外部シンボルとして定義されているのだが、Ubuntu で同じことをすると _rt0_amd64_linux_lib
は内部シンボルとして定義されていた。そして、FPC で procedure _rt0_amd64_windows_lib(); cdecl; external;みたいな形で定義したものは外部シンボルでなければ参照できず、このままでは無理、という状況。
「この問題を解決するためにどうするのがいいか」という以前にそもそもこの辺あまり詳しくないので、わかる範囲で少し調べながら対応方法を検討した。
その上で一番正しいっぽい道のりは多分「FPC 側でオブジェクトファイルの .init_array / .ctor セクションを参照して初期化処理を実行する」みたいな感じだと思うのだが、ちょっと話のスケールが大きい上に既存の振る舞いとの互換性の問題もありそうで手を出しにくいので、この方向は諦めた。
もし実現させるならプログラム固有のコードじゃなくてランタイム側のコードを変える話になりそう。
次に考えたのは「Go言語側でのビルド時に Windows では外部シンボルになっているんだから、挙動が違う原因を突き止めて両方外部シンボルにできれば解決するのでは?」という方向性。
しかし、そもそも外部シンボルになっているものを
nm -g libhoge.a
で列挙してみると、むしろ Windows 側が不必要な部分まで外部シンボルになってしまっているのではないか、と思えるぐらいに差があったので、将来的には逆に Windows 側で多くが内部シンボルになる方が正しそうな予感がして、このアプローチもやめた(だから挙動が違う原因もわからないまま)。最終的に選んだアプローチは「
_rt0_amd64_linux_lib
を外部シンボル化する方法を探る」という感じ。Linux 側で
nm -g libhoge.a
した時に出てくる cgo 関係の外部シンボルを見る限り、この辺りで使われている機能を上手く利用すれば外部シンボルにできそうだった。最初はGo言語のソースコードの src/runtime 辺りにファイルを追加して、全体を make し直して使えるようになることを確認していたのだが、もう少し調べてみると別に src/runtime に置く必要は全然なかったようなので、github.com/oov/rt0 という、コードが全然ないパッケージを作った。
darwin とか試してないものもあるが、取りあえず
_rt0_GOARCH_GOOS_lib
が現状定義されているものに関しては全部揃えておいた。これで、
-buildmode=c-archive
を使う main パッケージ辺りでimport _ "github.com/oov/rt0"だけで外部シンボルになるようになった。
初期化処理を正しく呼ぶ
Windows の時はパラメータを特に定義せずに呼んでいたのだが、同じように Linux で
_rt0_amd64_linux_lib
を呼んでみると、プログラムの起動時にruntime: kernel page size (134593276374883) is larger than runtime page size (4096)という、なんか値の差が大きすぎて何かが根本的に噛み合ってなさそうな気配のするエラーメッセージが出て上手く動かなかった。
Go 1.6.2 で同じことをしてもエラーメッセージが出ずに実行されたのだが、つい最近追加された部分だったらしい。
そこで rt0_linux_amd64.s のコードを読んでみると、tip の rt0_windows_amd64.s とは違い DI と SI には本来 argc と argv が入っているらしいことがわかった。呼び出し規約を確認してみると2つの引数が渡されてきた時の形なので「あ、これ要するに main 関数じゃん!」などとやっと気付いた勘の悪い自分であったが、結果的に
procedure _rt0_amd64_linux_lib(argc: Longint; argv: PPChar); cdecl; external; initialization _rt0_amd64_linux_lib(argc, argv);という形に辿り着くことができたので良しとしよう。
動いたー やったー |
処理をまとめる
これで Windows と Linux で同じ地点まで辿り着いたので、あとは同じソースコードで動くようにしたい。
まずはGo言語側。前回より少しメモリコピー回数がマシになっている。
OS によって処理が分かれている部分も特にないのであまり見どころはない。
package main // #include <string.h> import "C" import ( "bytes" "image/png" "os" "unsafe" "github.com/oov/psd" _ "github.com/oov/rt0" ) //export PSD2PNG func PSD2PNG(filename *C.char, buf *unsafe.Pointer, ln *C.int) C.int { file, err := os.Open(C.GoString(filename)) if err != nil { return 1 } defer file.Close() img, _, err := psd.Decode(file, &psd.DecodeOptions{SkipLayerImage: true}) if err != nil { return 2 } var b bytes.Buffer pe := png.Encoder{CompressionLevel: png.BestSpeed} err = pe.Encode(&b, img) if err != nil { return 3 } *buf = C.malloc(C.size_t(b.Len())) C.memcpy(*buf, unsafe.Pointer(&b.Bytes()[0]), C.size_t(b.Len())) *ln = C.int(b.Len()) return 0 } func main() {}そして Lazarus 側。初期化処理用のシンボル名は環境毎に変わるので、まずそれに対応する処理が必要。
あとは Windows の時はリンクするファイルが増えるのでそれも ifdef で囲っておく。
Windows では argc / argv を参照していないが、呼び出し規約的に動作に影響はないはずなので処理は分けてない。
unit psdtest; {$mode objfpc}{$H+} interface function PSD2PNG(Filename: PChar; Buf: PPointer; Len: PInteger): Integer; cdecl; external Name 'PSD2PNG'; procedure Cfree(P: Pointer); cdecl; external Name 'free'; implementation {$linklib libpsdtest.a} {$ifdef WINDOWS} {$linklib libntdll.a} {$linklib libws2_32.a} {$linklib libadvapi32.a} {$linklib libwinmm.a} {$linklib libkernel32.a} {$linklib libmsvcrt.a} {$endif} const ARCH = {$ifdef CPU386}'386'{$endif}{$ifdef CPUAMD64}'amd64'{$endif}{$ifdef CPUARM}'arm'{$endif}{$ifdef CPUAARCH64}'arm64'{$endif}; OS = {$ifdef WINDOWS}'windows'{$endif}{$ifdef LINUX}'linux'{$endif}{$ifdef DARWIN}'darwin'{$endif}; procedure GoInit(argc: longint; argv: PPChar); cdecl; external Name '_rt0_' + ARCH + '_' + OS + '_lib'; initialization GoInit(argc, argv); end.まあ Lazarus 側はこのままだと扱いにくいので、もうちょっと使いやすくしたのがこちら。
unit psdtest; {$mode objfpc}{$H+} interface uses Classes; function PSD2PNG(FileName: string): TStream; implementation uses SysUtils; function PSD2PNG(Filename: PChar; Buf: PPointer; Len: PInteger): integer; cdecl; external Name 'PSD2PNG'; procedure Cfree(P: Pointer); cdecl; external Name 'free'; {$linklib libpsdtest.a} {$ifdef WINDOWS} {$linklib libntdll.a} {$linklib libws2_32.a} {$linklib libadvapi32.a} {$linklib libwinmm.a} {$linklib libkernel32.a} {$linklib libmsvcrt.a} {$endif} const ARCH = {$ifdef CPU386}'386'{$endif}{$ifdef CPUAMD64}'amd64'{$endif}{$ifdef CPUARM}'arm'{$endif}{$ifdef CPUAARCH64}'arm64'{$endif}; OS = {$ifdef WINDOWS}'windows'{$endif}{$ifdef LINUX}'linux'{$endif}{$ifdef DARWIN}'darwin'{$endif}; procedure GoInit(argc: longint; argv: PPChar); cdecl; external Name '_rt0_' + ARCH + '_' + OS + '_lib'; // -------------------------------- type TCMemoryStream = class(TCustomMemoryStream) public constructor Create(const P: Pointer; Sz: PtrInt); destructor Destroy; override; end; constructor TCMemoryStream.Create(const P: Pointer; Sz: PtrInt); begin inherited Create; SetPointer(P, Sz); end; destructor TCMemoryStream.Destroy; begin Cfree(Memory); inherited Destroy; end; function PSD2PNG(FileName: string): TStream; var Errno, Size: Integer; Buf: Pointer; begin Errno := PSD2PNG(PChar(FileName), @Buf, @Size); if Errno <> 0 then raise Exception.Create('cannot convert psd to png: '+FileName); try Result := TCMemoryStream.Create(Buf,Size); except Cfree(Buf); end; end; initialization GoInit(argc, argv); end.ファイル名を string、戻り値を TStream にすることでフレンドリーに使える形になった。 TCustomMemoryStream を継承したストリームを作る事で無駄なメモリコピーも抑制できて一石二鳥。
入力もファイル名の代わりに TStream にできると思うけど、巨大なファイルをメモリの固まりじゃなくてストリームとして綺麗に渡すのは結構面倒な話になりそうなのでここでは触れないでおく。
使う側は
procedure TForm1.Button1Click(Sender: TObject); var S: TStream; begin if not OpenDialog1.Execute then Exit; S := PSD2PNG(OpenDialog1.FileName); try Image1.Picture.LoadFromStream(S); finally S.Free; end; end;みたいな普通な感じで使えて、これで両方の環境でコンパイルできるソースコードになった。