🤝

NimからGoで定義した関数やメソッドを呼び出す

2021/07/18に公開

はじめまして、しぶちゃりです。
今回はNimからGo言語のメソッドを呼び出す方法について紹介します。

NimのFFI

NimではCやC++、JavaScriptなど他の言語との連携(FFI)を容易に行うことができる特徴があります。

Nimの標準機能であるpragmaを用いることで実現することが可能です。
例えばCで定義したadd関数を利用したい場合

// add.c
int add(int a, int b)
{
	return a + b;
}

のようにソースコードを作成した後に共有ライブラリとしてコンパイルします。

gcc -shared -o add.dll add.c 
proc add(a: int, b: int): int {.dynlib: "add.dll", importc: "add" .}

echo add(1, 2)

の様にすることで利用することが可能です。

GoをCと連携するcgoパッケージ

Go言語にはCと連携するためのパッケージとしてcgoというものが提供されています。
cgoを用いることで、Cにあるメソッドやデータ型をGoで利用できたり、逆にGoで定義したメソッドなどをCで利用することも可能です。

上記と同様にaddメソッドをGoで定義してみます。

package main

import "C"

//export add
func add(a, b int) int {
	return a + b
}

func main() {}

cgoでは //export ディレクティブを用いることでヘッダーファイルにexternすることができます。
まずはビルドします。私の環境はm1 macでアーキテクチャがarm64のため、GOARCHをarm64としています。
この後の連携で用いるNimもarm64で動くため、連携間でのアーキテクチャが統一されていないと当然エラーになります。
またクロスコンパイルではCGO_ENABLEDがデフォルトで有効(1)にならないため、明示的に有効にしています。

GOARCH=arm64 CGO_ENABLED=1  go build -buildmode=c-shared -o main.dll main.go

実際にビルドして生成されたヘッダーファイルが以下のものです。

/* Code generated by cmd/cgo; DO NOT EDIT. */

/* package command-line-arguments */


#line 1 "cgo-builtin-export-prolog"

#include <stddef.h> /* for ptrdiff_t below */

#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif

#endif

/* Start of preamble from import "C" comments.  */




/* End of preamble from import "C" comments.  */


/* Start of boilerplate cgo prologue.  */
#line 1 "cgo-gcc-export-header-prolog"

#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef __SIZE_TYPE__ GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;

/*
  static assertion to make sure the file is being used on architecture
  at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

#endif

/* End of boilerplate cgo prologue.  */

#ifdef __cplusplus
extern "C" {
#endif

extern GoInt add(GoInt a, GoInt b);

#ifdef __cplusplus
}
#endif

cgoを用いて生成したソースをNimから呼び出す

あとは生成されたファイルを先程NimからCを呼び出したように指定してあげるだけで、利用できます。

proc add(a: int, b: int): int {.dynlib: "add.dll", importc: "add".}

echo add(1, 2)

おまけ

実用性があるかは不明ですが、Nimからgoroutineを利用することもできます

package main

import "C"
import (
	"context"
	"fmt"
	"time"
	"unsafe"
)

var boxInstances map[uintptr]*context.Context

func main() {}

//export newCtx
func newCtx() uintptr {
	ctx := context.Background()
	p := uintptr(unsafe.Pointer(&ctx))
	if boxInstances == nil {
		boxInstances = make(map[uintptr]*context.Context)
	}
	boxInstances[p] = &ctx
	return p
}

//export parent
func parent(p uintptr) {
	// parentからcontextを生成し、childに渡す
	c := (*context.Context)(unsafe.Pointer(p))
	ctx, cancel := context.WithTimeout(*c, time.Second*3)
	go child(ctx, "Hello-child")
	defer cancel()
	// 無限ループ
	for {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Err())
			return
		default:
			fmt.Println("parent Nim")
			time.Sleep(time.Millisecond * 500)
		}
	}
}

func child(ctx context.Context, str string) {
	// 無限ループ
	for {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Err(), str)
			return
		default:
			fmt.Println("child Nim")
			time.Sleep(time.Millisecond * 500)
		}
	}
}
GOARCH=arm64 CGO_ENABLED=1  go build -buildmode=c-shared -o goroutine.dll main.go
type
  P = pointer

proc newCtx():P {.dynlib: "goroutine.dll", importc: "newCtx".}
proc parent(p: P):P {.dynlib: "goroutine.dll", importc: "parent".}

let ctx = newCtx()
discard ctx.parent

実行結果

parent Nim
child Nim
child Nim
parent Nim
child Nim
parent Nim
child Nim
parent Nim
parent Nim
child Nim
child Nim
parent Nim
context deadline exceeded
context deadline exceeded Hello-child

最後までお読みいただきありがとうございました。

Discussion