Go言語の Windows 版で -buildmode=c-archive を使う
安定版のリリースはまだ先だけど、tip で Windows 上での -buildmode=c-archive が動くようになったらしい。早速試してみることにする。
1. 安定版のGo言語をインストール
tip のGo言語をビルドするために、まず安定版をインストールする。
これは普通にインストーラー版を使ってインストールするだけでいい。
2. GCC をインストール
後々のために入れる。
build.golang.org で動いている Windows のビルダーは winstrap で環境を構築しているようなのだが、ここを見る限りでは TDM-GCC を使っているようなので、これを入れることにする。
3. Git をインストール
このご時世インストールしてない人はいないと思うけども。
これもインストーラから。
4. tip 版のGo言語をビルド
ここまでの段階でインストーラによって環境変数
この安定版を最新版のビルドに使うことを示すために、set GOROOT_BOOTSTRAP=%GOROOT% などとして環境変数に入れておく。
そして適当な深すぎないディレクトリで git clone https://github.com/golang/go/ でGo言語をクローンして、go\src\all.bat を実行すればビルドは始まる。
ビルドが完了したら、今度は最新版をGo言語を使うために set GOROOT=clone先 として
改めて go version して go version devel という感じでバージョン情報が変わっていたら準備完了。
5. テストプログラムを作る
c-archive モードを使うためのテストプログラムを作ってみる。
適当なディレクトリを作って、その中にこんな感じのファイルを作成する。
今回は carchivetest というディレクトリの中に main.go というファイル名で作成した。
文字列を返したい場合、そのまま
ちなみに文字列を返さない場合など、この例では
なお、ここでは
main.go があるディレクトリで go build -buildmode=c-archive とすると、carchivetest.h carchivetest.a というファイルが出力される。
次にC言語側のプログラムを作る。
生成されたヘッダーファイルを使って、エクスポートされた関数を呼び出す。
これをコンパイルするには
これで a.exe が生成されているので、実行すると以下のような出力が得られる。
6. テストプログラムをもっと作る
せっかく Windows 上で使えるようになったんだし、GUI プログラムでも試してみようと思う。
今度は GCC じゃなくて Lazarus を使ってプログラムを作ってみる。
Lazarus は 1.6 の 64bit 版をインストール(Go言語も 64bit 版を使っていたから)。
ある程度試してみたところ、以下のような感じにすると動いた。
このユニットファイルを Unit1 の uses 句に加え、ボタンを一個配置し、そのボタンをダブルクリックし、追加されたコードを以下の様に書き加える。
7. テストプログラムをもっともっと作る
ここまで動くようになったら、もう少し発展的な実験もしてみたい。
最近Go言語用の Photoshop 形式の画像読み込みライブラリを作ったので、これを使って画像データを読み込んでみる。
改めて libpsdtest というディレクトリを作成し、その中に以下のファイルを main.go として作成。
Go言語側で戻り値を複数返した場合、Cのヘッダーファイル上で構造体が定義され、それが返されるような形になる。しかし FPC 側でそれを正しく受け取る方法がよくわからないので、今回は素直にポインタを複数渡すことにした。
これを呼ぶためのユニットファイルは以下の様な形。
そしてまた新規プロジェクトに今度は TOpenDialog と TImage と TButton をフォーム上に配置して、ボタンのクリックハンドラを以下のように実装。
しかしプログラムを走らせて色々な PSD ファイルを読み込ませていたところ、どうも runtime.deductSweepCredit の中で SIGFPE で動作が停止してしまうことがたまにあった。
ここだとすると383行目で
そういう状態で自分の手には余る感じなので問題は解決できておらず、ひとまずマスクして回避するために
この PSD2PNG プログラムのソースコードとコンパイル済みバイナリはこちら。
1. 安定版のGo言語をインストール
tip のGo言語をビルドするために、まず安定版をインストールする。
これは普通にインストーラー版を使ってインストールするだけでいい。
2. GCC をインストール
後々のために入れる。
build.golang.org で動いている Windows のビルダーは winstrap で環境を構築しているようなのだが、ここを見る限りでは TDM-GCC を使っているようなので、これを入れることにする。
3. Git をインストール
このご時世インストールしてない人はいないと思うけども。
これもインストーラから。
4. tip 版のGo言語をビルド
ここまでの段階でインストーラによって環境変数
GOROOT
が設定されていると思うので、コマンドプロンプトを開いて go version を打てば安定版のGo言語のバージョンが表示されるはず。この安定版を最新版のビルドに使うことを示すために、set GOROOT_BOOTSTRAP=%GOROOT% などとして環境変数に入れておく。
そして適当な深すぎないディレクトリで git clone https://github.com/golang/go/ でGo言語をクローンして、go\src\all.bat を実行すればビルドは始まる。
ビルドが完了したら、今度は最新版をGo言語を使うために set GOROOT=clone先 として
GOROOT
を書き換えて、更に set PATH=%GOROOT%\bin;%PATH% として PATH
も再優先で通しておく。改めて go version して go version devel という感じでバージョン情報が変わっていたら準備完了。
5. テストプログラムを作る
c-archive モードを使うためのテストプログラムを作ってみる。
適当なディレクトリを作って、その中にこんな感じのファイルを作成する。
今回は carchivetest というディレクトリの中に main.go というファイル名で作成した。
package main import ( "C" "log" ) //export testInt func testInt() int { log.Println("testInt") return 42 } //export testStr func testStr() *C.char { log.Println("testStr") return C.CString("OK") } func main() { log.Println("Hello world") }
//export
から始まるコメント行はエクスポートすることを明示するアノテーションなので必ず必要。文字列を返したい場合、そのまま
string
を渡すと GoString
型になるけど、C.CString
で変換すると char
ポインタで渡すこともできる。この場合受け取った側で free
する必要がある。ちなみに文字列を返さない場合など、この例では
import "C"
がなくてもコンパイルはできるものの、その場合C言語側から呼ぶ時に必要になるヘッダーファイルが生成されないし多分エクスポートも動かない気がするので import "C"
は必須だと思う。なお、ここでは
main
で Hello world を出力するようなコードを書いているが、main は無視されるはずなので実際には出力されない。main.go があるディレクトリで go build -buildmode=c-archive とすると、carchivetest.h carchivetest.a というファイルが出力される。
次にC言語側のプログラムを作る。
生成されたヘッダーファイルを使って、エクスポートされた関数を呼び出す。
#include <stdio.h> #include <stdlib.h> #include "carchivetest.h" int main(int argc, char *argv[]) { printf("main\n"); printf("testInt -> %lld\n", testInt()); char *r = testStr(); printf("testStr -> %s\n", r); free(r); }特に意外性のない普通のプログラム。
これをコンパイルするには
gcc main.c carchivetest.a -lntdll -lws2_32
という感じにする。これで a.exe が生成されているので、実行すると以下のような出力が得られる。
main 2016/04/04 18:30:16 testInt testInt -> 42 2016/04/04 18:30:16 testStr testStr -> OK予定通り
"Hello world"
は出力されず、それぞれに書いた出力は正しく出力されている。6. テストプログラムをもっと作る
せっかく Windows 上で使えるようになったんだし、GUI プログラムでも試してみようと思う。
今度は GCC じゃなくて Lazarus を使ってプログラムを作ってみる。
Lazarus は 1.6 の 64bit 版をインストール(Go言語も 64bit 版を使っていたから)。
ある程度試してみたところ、以下のような感じにすると動いた。
- FPC のリンカだと上手く動かないので Use external linker オプションを付ける必要がある
→ プロジェクトオプションのコンパイラオプション → Custom Options から -Xe を追加した - ライブラリのファイル名が lib*.a でなければ上手く認識させられなかった
→ 単純に carchivetest.a を libcarchivetest.a をリネームした - TDM-GCC に付属している libntdll.a libws2_32.a libadvapi32.a libkernel32.a libmsvcrt.a にリンクする必要があるようなので、ライブラリパスを追加する
→ プロジェクトオプションのコンパイラオプション → パス のライブラリにパスを追加 - Go言語側の初期化処理のために
initialization
句辺りで_rt0_amd64_windows_lib
を呼ばなければいけない
→ 呼ばない場合エクスポートされた関数を呼び出しても処理が返ってこなくなる - 文字列をC言語のサンプルプログラムのように受け取る場合、
free
もC言語のランタイム側で定義されているのを呼ばないと多分駄目
→ 定義して呼べるようにしておく
unit carchivetest; {$mode objfpc}{$H+} interface function testInt(): Int64; cdecl; external name 'testInt'; function testStr(): PChar; cdecl; external name 'testStr'; procedure Cfree(P: Pointer); cdecl; external name 'free'; implementation {$linklib libcarchivetest.a} {$linklib libntdll.a} {$linklib libws2_32.a} {$linklib libadvapi32.a} {$linklib libkernel32.a} {$linklib libmsvcrt.a} procedure _rt0_amd64_windows_lib(); cdecl; external; initialization _rt0_amd64_windows_lib(); end.新規プロジェクトからアプリケーションを選び新規プロジェクトを作成し、更にファイルメニューから新規ユニットを加えて上記のユニットファイルを作成。
このユニットファイルを Unit1 の uses 句に加え、ボタンを一個配置し、そのボタンをダブルクリックし、追加されたコードを以下の様に書き加える。
procedure TForm1.Button1Click(Sender: TObject); var P: PChar; begin P := testStr(); Caption := IntToStr(testInt()) + ' / ' + P; Cfree(P); end;これでコンパイル・実行して、ボタンを押した時のスクリーンショットが以下のもの。
![]() |
Caption にGo言語側から渡した値が正しく表示されている |
ここまで動くようになったら、もう少し発展的な実験もしてみたい。
最近Go言語用の Photoshop 形式の画像読み込みライブラリを作ったので、これを使って画像データを読み込んでみる。
改めて libpsdtest というディレクトリを作成し、その中に以下のファイルを main.go として作成。
package main import ( "C" "bytes" "image" "image/png" "os" _ "github.com/oov/psd" ) //export PSD2PNG func PSD2PNG(filename *C.char, buf **C.char, ln *int, errno *int) { file, err := os.Open(C.GoString(filename)) if err != nil { *errno = 1 return } defer file.Close() img, _, err := image.Decode(file) if err != nil { *errno = 2 return } var b bytes.Buffer err = png.Encode(&b, img) if err != nil { *errno = 3 return } *buf = C.CString(b.String()) *ln = b.Len() *errno = 0 } func main() {}プログラムとしてはやはり単純。
Go言語側で戻り値を複数返した場合、Cのヘッダーファイル上で構造体が定義され、それが返されるような形になる。しかし FPC 側でそれを正しく受け取る方法がよくわからないので、今回は素直にポインタを複数渡すことにした。
これを呼ぶためのユニットファイルは以下の様な形。
unit psdtest; {$mode objfpc}{$H+} interface procedure PSD2PNG(Filename: PChar; Buf: PPointer; Len: PInteger; Errno: PInteger); cdecl; external Name 'PSD2PNG'; procedure Cfree(P: Pointer); cdecl; external Name 'free'; implementation uses Math; {$linklib libpsdtest.a} {$linklib libntdll.a} {$linklib libws2_32.a} {$linklib libadvapi32.a} {$linklib libkernel32.a} {$linklib libmsvcrt.a} procedure _rt0_amd64_windows_lib(); cdecl; external; initialization SetExceptionMask([exInvalidOp, exPrecision]); _rt0_amd64_windows_lib(); end.目新しい点は SetExceptionMask なのだが、これは後述する。
そしてまた新規プロジェクトに今度は TOpenDialog と TImage と TButton をフォーム上に配置して、ボタンのクリックハンドラを以下のように実装。
procedure TForm1.Button1Click(Sender: TObject); var Buf: Pointer; Len, Errno: integer; MS: TMemoryStream; begin if not OpenDialog1.Execute then Exit; PSD2PNG(PChar(OpenDialog1.FileName), @Buf, @Len, @Errno); if Errno <> 0 then Exit; try MS := TMemoryStream.Create; try MS.WriteBuffer(Buf^, Len); MS.Position := 0; Image1.Picture.LoadFromStream(MS); finally MS.Free; end; finally Cfree(Buf); end; end;実行させた結果がこちら。
![]() |
後ろの FireAlpaca で読み込んでいるのと同じ PSD ファイルを読み込んで表示している |
![]() |
止まるときは毎回ここらしい |
int64
に変換している辺りの部分でコケているっぽいことになりそうだけど、ちょっと状況がよくわからない。exInvalidOp
をマスクすれば止まらなくなるものの、そもそもなんで止まるのか、普通にGo言語で実行している時は起きているのか、そのへんがよくわからない。そういう状態で自分の手には余る感じなので問題は解決できておらず、ひとまずマスクして回避するために
SetExceptionMask([exInvalidOp, exPrecision])
を入れている、というわけ。この PSD2PNG プログラムのソースコードとコンパイル済みバイナリはこちら。