(本記事はGo3 Advent Calendar 15日目の記事になります。穴が空いていたようなので、埋めさせていただきます)
import "syscall"
愛用者の皆さん、こんにちは。
Windows の API を使う時、import "cgo"
ではなく、import "syscall"
で DLL をロードするようにすると、ビルドの際、C言語のコンパイラが不要になります。ビルド要件が下がると、それだけ開発したライブラリを使ってもらう確率もあがりますし、プルリクとかもしてもらいやすくなります。なので、少々の手間をかけて import "syscall"
を使う価値は高いと思います。
でも…関数本体の宣言はともかくとして、たくさんの定数をヘッダファイルから書き写すのはたいへんですよね。
そこで、その手間を節約するツール:go-importconst を開発しました。
go-importconst とは
package dos
//go:generate go run github.com/zetamatta/go-importconst
// <windows.h>
// RESOURCE_CONNECTED
// RESOURCE_CONTEXT
// RESOURCE_GLOBALNET
// RESOURCE_REMEMBERED
// RESOURCETYPE_ANY
// RESOURCETYPE_DISK
// RESOURCETYPE_PRINT
// RESOURCEDISPLAYTYPE_NETWORK
// RESOURCEDISPLAYTYPE_SERVER
// RESOURCEUSAGE_CONNECTABLE
// RESOURCEUSAGE_CONTAINER
// RESOURCEUSAGE_ATTACHED
// RESOURCEUSAGE_ALL
// ERROR_NO_MORE_ITEMS
// CONNECT_UPDATE_PROFILE
// S_OK
というコメントコード(const.go
)から
package dos
// Code generated by importconst.go DO NOT EDIT.
const RESOURCE_CONNECTED = 1
const RESOURCE_CONTEXT = 5
const RESOURCE_GLOBALNET = 2
const RESOURCE_REMEMBERED = 3
const RESOURCETYPE_ANY = 0
const RESOURCETYPE_DISK = 1
const RESOURCETYPE_PRINT = 2
const RESOURCEDISPLAYTYPE_NETWORK = 6
const RESOURCEDISPLAYTYPE_SERVER = 2
const RESOURCEUSAGE_CONNECTABLE = 1
const RESOURCEUSAGE_CONTAINER = 2
const RESOURCEUSAGE_ATTACHED = 16
const RESOURCEUSAGE_ALL = 19
const ERROR_NO_MORE_ITEMS = 259
const CONNECT_UPDATE_PROFILE = 1
const S_OK = 0
という実際の値を定義した実コード(zconst.go
)を生成する go generate 用ツールです。
go generate を実行する時は C++コンパイラが必要となりますが、一度生成した zconst.go
は C++ コンパイラなしに利用できます。
動作原理
(1)
Goのソースのコメントコードを読み取って、対象となる定数名を得る
(2)
次のような C++ のソースを作成する
#include <cstdio>
#include <windows.h>
void p(const char *name,const char *s){
printf("const %s=\"%s\"\n",name,s);
}
void p(const char *name,int n){
printf("const %s=%d\n",name,n);
}
void p(const char *name,long n){
printf("const %s=%ld\n",name,n);
}
void p(const char *name,unsigned long n){
printf("const %s=%ld\n",name,n);
}
void p(const char *name,double n){
printf("const %s=%lf\n",name,n);
}
int main()
{
printf("package dos\n\n");
printf("// Code generated by importconst_run.go DO NOT EDIT.\n");
p("RESOURCE_CONNECTED",RESOURCE_CONNECTED);
// 中略
p("S_OK",S_OK);
return 0;
}
(3)zconst.cpp を C++ コンパイラでコンパイルする
(4)生成された実行ファイル (a.out
or a.exe
)を呼び出して、標準出力を zconst.go
へリダイレクトする
(5)go fmt
する
ね、かんたんでしょ?
(1)Goのソースのコメントコードを読み取って、対象となる定数名を得る
実は importconst
はちょっと前までバッチファイルから呼び出すようになっていたのですが、コマンドラインパラメータにパッケージ名・定数名を全部書かなくちゃいけないので、狭っ苦しくて見栄えが悪くなっていました。
go generate から呼び出すのであれば、Goのソース中にそうした情報を書いた方が綺麗です。
go help generate
によると
- 環境変数
%GOFILE%
に go generate が書かれているソースのファイル名 - 環境変数
%GOLINE%
に go generate が書かれているソースの行位置 - 環境変数
%GOPACKAGE%
にパッケージ名
が入っているそうです。これらを利用して、go generate が書かれた行以降のコメント行に定数名を並べてゆくような文法にするのがベストでしょう!
そして、定数が <nnnnn>
や xxxxxx.h
という名前だった場合、それは定数名ではなく、#include
すべきヘッダファイル名と解釈させます(<cstdio>
だけは例外で常に include します)
(2)C++ のソースを作成する
もう出力例を書いてしまったので、あまり書くことはありませんね。定数の型に応じて、出力の仕方を変えられるよう、出力用関数p
を多重定義しているところがアイデアでしょうか。
(それも <iostream>
を使えばいらないんですが、 重いから…)
(3)コンパイルする
gcc 呼ぶだけなので、普通に os.exec を使います。
(4)a.out/a.exe を呼び出す
同じく呼ぶだけなので、こちらも os.exec を使います。ただ、OS によってコンパイルの出力するデフォルト実行ファイル名が違うので、runtime.GOOS で判断します。
(このツールは Windows でしか使わないと思いますが、どこでも動作できるように越したことはないでしょう)
func nameOfExecutable() string {
if runtime.GOOS == "windows" {
return "a.exe"
} else {
return "a.out"
}
}
そして、リダイレクト先を zconst.go
とするのです。
(5)go fmt zconst.go
を呼び出す
最初からちゃんと整形済み出力すればいいんですが、ちょっとしたことでズレたりしがちになってしまうので、素直に go fmt
を読んだほうが安全です。
ここまで来たら、あとは一時的に作ったテンポラリファイル群(zconst.cpp
,a.out
,a.exe
)したらお終いです。ありがとうございました。
最後に
最近は多くの Windows-API や定数は golang.org/x/sys/windows
に定義済みになってしまいましたが、まだ全部網羅されているわけではありません。importconst
が便利なシーンはまだまだあるでしょう。
以上