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言語をビルド

ここまでの段階でインストーラによって環境変数 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" は必須だと思う。
なお、ここでは mainHello 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言語のランタイム側で定義されているのを呼ばないと多分駄目
    → 定義して呼べるようにしておく
以上の点を踏まえた上で、carchivetest を呼ぶためのユニットファイルは概ね以下の様な形になった。
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言語側から渡した値が正しく表示されている
7. テストプログラムをもっともっと作る

ここまで動くようになったら、もう少し発展的な実験もしてみたい。
最近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 ファイルを読み込んで表示している
しかしプログラムを走らせて色々な PSD ファイルを読み込ませていたところ、どうも runtime.deductSweepCredit の中で SIGFPE で動作が停止してしまうことがたまにあった。
止まるときは毎回ここらしい
ここだとすると383行目で int64 に変換している辺りの部分でコケているっぽいことになりそうだけど、ちょっと状況がよくわからない。exInvalidOp をマスクすれば止まらなくなるものの、そもそもなんで止まるのか、普通にGo言語で実行している時は起きているのか、そのへんがよくわからない。
そういう状態で自分の手には余る感じなので問題は解決できておらず、ひとまずマスクして回避するために SetExceptionMask([exInvalidOp, exPrecision]) を入れている、というわけ。

この PSD2PNG プログラムのソースコードとコンパイル済みバイナリはこちら