モチベーション
メリット
- 一回書けば Win/Mac 両方で動作する(Write once, run anywhere)
- Java と違い各プラットフォームネイティブのバイナリが出力される
- ネイティブのバイナリが出力されるので Java より高速
- 実装が統一される
デメリット
主として Windows 側に集中しています。
- Windows でのビルド方法がやや特殊
- ひと手間(非公式の手段)かける必要性があり若干の懸念あり
- これは、Go が Windows 向けの共有ライブラリを生成できない(
buildmode=c-shared
を非サポート)のと、C# が Win32 形式での静的ライブラリを利用できないためです。
- Windows(と言うか C# というか CLR)では C# で書くよりパフォーマンスの懸念あり
- アンマネージド DLL の呼出しなのでマーシャリングのコストが余計にかかるのではとの予想です。
- マルチプラットフォーム対応でなく速度重視なら、おとなしく C#(というか CLR で動作する言語)で書いた方が高速だと思います。
Windows
呼び出し側は C# をターゲットにしています。
Go ライブラリのコード
package main
import (
"C"
"fmt"
"gopkg.in/resty.v0"
)
//export GetUsers
func GetUsers(p *C.char) *C.char {
fmt.Println(C.GoString(p))
resp, err := resty.R().
SetHeader("Accept", "application/json").
Get("https://jsonplaceholder.typicode.com/users")
if err != nil {
return C.CString("")
}
return C.CString(resp.String())
}
func init() {}
func main() {}
適当なウェブサービスを呼び出して、その結果を JSON で受け取り文字列として返却する関数です。
-
関数のコメント重要
- ここでエクスポートを宣言している(cgo)
- C 言語を意識する
- データ型の変換に注意
Go ライブラリのビルド
この手順は公式の方法です。
go build -buildmode=c-archive rest.go
DLL のビルド
この手順は非公式な方法です。
以下のツールをインストールしてください。
-
TDM-GCC
- 環境変数
PATH
にbin/
のパスを通してください。 - 一時的に設定する場合は
set PATH=%PATH%;C:\TDM-GCC-64\bin
です。
- 環境変数
-
Visual Studio Community
- インストール後に C++ のプロジェクトを作成して関連ツールをインストールしてください。
まずモジュール定義 (.def) ファイルを作成します。
LIBRARY rest
EXPORTS
GetUsers
関数は EXPORTS
以下に追加していきます。
DLL をビルドします。
gcc -m64 -shared -o rest.dll rest.def rest.a -Wl,--allow-multiple-definition -static -lstdc++ -lwinmm -lntdll -lWs2_32
C# から DLL を呼び出す
Visual Studio での注意
- IDE の参照メニューからは DLL への参照を追加できないので(アンマネージドだから?)、実行ファイル(
.exe
)があるフォルダ(Debug
)にビルドした DLL をコピーします(実行パスにある DLL はデフォルトで読み込まれる動作を利用)。 - プラットフォーム構成の種類が
Any CPU
だと実行時例外が発生するので、x64
に変更します(DLL がx64
向けにビルドされているので)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SampleGoDLL
{
static class Program
{
[DllImport("rest.dll")]
public static extern void PrintHello(string pStr);
[DllImport("rest.dll")]
public static extern IntPtr GetUsers(string pStr);
/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
IntPtr pRet = GetUsers("Call from C#");
String json = Marshal.PtrToStringAnsi(pRet);
Console.Write(json);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
アプリケーションの起動時に実行されるようにしました。
関数の外部参照宣言(extern
)毎に DLLImport
属性が必要です。
C# に関する詳細はインターネット上の情報を参照してください。
その他
DLL の検証
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\x86_amd64
を環境変数 PATH
に追加しておくと便利です。
- DLL で宣言されている関数の確認する
dumpbin.exe /exports rest.dll
- DLL のターゲットアーキテクチャを確認する
dumpbin.exe /headers rest.dll | findstr machine
macOS
呼び出し側は Objective-C をターゲットにしています。
Go ライブラリのコードおよび Go ライブラリのビルドは Windows と共通です。
macOS の場合は共有ライブラリの出力も可能ですが、Windows に合わせて静的ライブラリを出力します。
Objective-C から静的ライブラリを呼び出す
Xcode のプロジェクト設定
Xcode の詳しい使い方はインターネット上の情報を参照してください。
- 静的ライブラリ(
rest.a
)を追加します。 - ヘッダーファイル(
rest.h
)を追加します。
#import "ViewController.h"
#import "rest.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
char *pStr = GetUsers("Call from Objective-C");
NSLog(@"%s", pStr);
}
@end
ビューのロード時に実行されるようにしました。
ポイントはヘッダーファイルのインポートです。
最後に
Go は高速性とか並列性とかが注目されがちですが、こういう便利な機能ももっと注目されて良いと思います。