NimからCの関数を呼び出す
久しぶりの投稿になります。NimからC関数を呼び出せる事は以前から知っていましたが、どの程度出来るかを検証してみました。
Nim言語は俗に言うトランスパイル言語です。つまりコンパイル時に、C言語への変換を行い、Cコンパイラで実行モジュールを作成するため、C言語との親和性がとても高い言語だと思います。
C言語との連携は、FFI(Foreign function interface)を介してC言語との連携を行います。
通常、どんな言語でも、オブジェクトやライブラリをリンケージして呼び出す事が可能ですが、NimはC言語のソースをプラグマを使用して直接呼び出し事が可能です。
C言語との連携には、埋め込み型と外部参照型があります。
- 埋め込み型は、プラグマemitを使用してNim言語のソース内にC言語を書く方法です。
- 外部参照型は、プラグマheaderを利用して、参照を行う方法です。
※今回確認するのは、埋め込み型ではなく、外部参照型で確認します。
環境
動作環境は以下の通りです。
- OS: Windows11
- Nim 2.0.4
また、プロジェクトフォルダにはCソースとNimソースを分けて管理します。
単純にプロジェクトフォルダの下に、csrcと言うフォルダを作成しただけですけどね。
引数の対応表
Nim言語とC言語では型が異なりますが、引数の型を合わせるために、cintやcstringなどの変数の型が存在します。
Nim言語 | C言語 | 内容 |
---|---|---|
cint | int | 符号有りのint32 |
cuint | unsigned int | 符号無しのint32 |
cchar | char | 1バイトキャラクタ文字 |
cstring | char * | キャラクター型のポインタ |
ptr cstring | char ** | キャラクター型のポインタ配列 |
cstringArray | char ** | 同上 |
pointer | void * | ポインタ型 |
※float,long,double型はどうやるのかは、調べていません。
C言語の関数を呼び出す場合
1. 単純にNimから引数無しのC関数を呼び出す
C言語のソースをcsrc/clang_sample1.hとして以下に記述します。Cソースファイルの拡張子を.hにする理由は、NimからCソースを呼ばれる時に、includeでソースを呼び出してるだけなので、ソースの拡張子は、.cではなく.hにした方がいいです。また、ifdefを使って2度読みさせないようにします。
#ifndef CLANG_SAMPLE1_H
#define CLANG_SAMPLE1_H
#include "stdio.h"
/* 関数宣言: C側のソースが複雑になれば関数宣言を入れないとエラーになる事もあります。 */
void test01_print();
/* 引数無しによる標準出力 */
void test01_print() {
printf("Nim言語からc言語の関数をアクセス\n");
return;
}
#endif
Nim側はソース名をsample1.nimに設定し、以下を記述。
Nim側はC関数指定時に、headerプラグマを使用して呼び出します。この場合、カレントパス指定でも良いのですが、わかりやすくフルパス指定にします。
また、C関数指定には、importcプラグマの後に関数名を書きますが、関数名が同じなら無くても問題ありません。
# C言語のソースファイル名を指定 (フルパスを指定)
from std/os import parentDir, `/`
const clangHeader = currentSourcePath().parentDir()/"csrc/clang_sample1.h"
# 関数定義 (連携する関数名を定義)
proc test01_print() {.header: clangHeader, importc:"test01_print".}
# 標準出力テスト
test01_print()
実行結果
$ nim r --hints:off .\sample1.nim
Nim言語からc言語の関数をアクセス
プログラムの説明
ただ単に、引数無しの関数を呼び出してるだけの単純な処理ですが、Cのソースファイルには関数宣言があった方が良いでしょう。ソースが複雑になれば読み込めないなどの弊害が出てきますので。
2. 引数がint型のC関数を呼び出し
C言語のソースをcsrc/clang_sample2.hとして以下に記述します。
#ifndef CLANG_SAMPLE2_H
#define CLANG_SAMPLE2_H
#include "stdio.h"
/* 関数宣言 */
int test02_print(int i);
/* 引数intによる標準出力 */
int test02_print(int i) {
printf("Nim言語からc言語の関数をアクセス Part2 i=%d\n", i);
return(i+1);
}
#endif
Nim側はソース名をsample2.nimに設定し、以下を記述。
また、C関数への引数は、cint型を設定します。
# C言語のソースファイル名を指定
from std/os import parentDir, `/`
const clangHeader = currentSourcePath().parentDir()/"csrc/clang_sample2.h"
# 関数定義 (連携する関数名を定義)
proc test02_print(i: cint): cint {.header: clangHeader, importc:"test02_print".}
# 標準出力テスト
var value: int32 = 5 # cint型はint32なので
echo "return=", $test02_print(value)
実行結果
$ nim r --hints:off .\sample2.nim
Nim言語からc言語の関数をアクセス Part2 i=5
return=6
プログラムの説明
注意する点は、Cの関数への受け渡しは、cintを用いる事です。また、C言語側のintは32ビットなのでint32に設定する必要があります。
3. 引数がchar型のC関数を呼び出し
C言語のソースをcsrc/clang_sample3.hとして以下に記述します。
#ifndef CLANG_SAMPLE3_H
#define CLANG_SAMPLE3_H
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
/* 関数宣言 */
char *test03_strcat(char *str1, char *str2);
/* 引数charによる標準出力 (単にアルファベット文字列を返すだけです) */
char *test03_strcat(char *str1, char *str2) {
printf("Nim言語からc言語の関数をアクセス Part3 str=%s\n", str1);
printf("Nim言語からc言語の関数をアクセス Part3 str=%s\n", str2);
char *rtn;
int SIZE = strlen(str1) + strlen(str2);
rtn = (char *)calloc(SIZE, sizeof(char));
strcpy(rtn, str1);
strcat(rtn, str2);
return( rtn );
}
#endif
Nim側はソース名をsample3.nimに設定し、以下を記述。
また、C関数への引数は、cstring型を設定します。
# C言語のソースファイル名を指定
from std/os import parentDir, `/`
const clangHeader = currentSourcePath().parentDir()/"csrc/clang_sample3.h"
# 関数定義 (連携する関数名を定義)
proc test03_strcat(s1, s2: cstring): cstring {.header: clangHeader, importc:"test03_strcat".}
# char型の引数 受け渡し
var rtn = test03_strcat("引数1", "引数2")
echo "return=", rtn
dealloc( rtn ) # free cstringにしないとエラーになる
実行結果
$ nim r --hints:off .\sample3.nim
Nim言語からc言語の関数をアクセス Part3 str=引数1
Nim言語からc言語の関数をアクセス Part3 str=引数2
return=引数1引数2
プログラムの説明
C言語側でメモリを確保し、値を連結させているので、deallocでメモリを解放しています。
4. 引数がint配列のC関数を呼び出し
C言語のソースをcsrc/clang_sample4.hとして以下に記述します。
#ifndef CLANG_SAMPLE4_H
#define CLANG_SAMPLE4_H
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
/* 関数宣言 */
int *test04_array(int *a, int size);
/* 引数に配列を渡す */
int *test04_array(int *a, int size) {
int *b = (int *)calloc(size, sizeof(int));
for(int i = 0; i < size; i++) {
b[i] = a[i] - 1;
printf("%d ", a[i]);
}
printf("\n");
return b;
}
#endif
Nim側はソース名をsample4.nimに設定し、以下を記述。
また、C関数への引数は、pointer型を設定します。
# C言語のソースファイル名を指定
from std/os import parentDir, `/`
const clangHeader = currentSourcePath().parentDir()/"csrc/clang_sample4.h"
# 関数定義 (連携する関数名を定義)
proc test04_array(a: pointer, s: cint): pointer {.header: clangHeader, importc:"test04_array".} # ptr cintではエラーになる
# 配列を引数に渡す
var ar: array[4, int32] = [10, 9, 8, 7] # int32型にしないとc言語側で認識しない
var rtn = test04_array(addr ar, cast[cint](ar.len))
var out = cast[ptr array[4, int32]](rtn)
# 出力結果の表示
for i in countup(0, 3):
stdout.write(" ", out[i])
stdout.writeLine("")
実行結果
$ nim r --hints:off .\sample4.nim
10 9 8 7
9 8 7 6
プログラムの説明
C関数へint配列を渡す場合は、pointer型で(addr ar)の設定して渡しています。
また、受取もpointer型であるため、キャストしてから値を取り出します。
元はNim と C の間で配列を渡したい。と言う記事を参考に記載しましたが、この記事では、やたらキャストが多かったので、簡単にまとめるには、pointer引数を指定するのが良いかと思いやってみました。
5. 引数が構造体を受け渡しのC関数を呼び出し
C言語のソースをcsrc/clang_sample5.hとして以下に記述します。
#ifndef CLANG_SAMPLE5_H
#define CLANG_SAMPLE5_H
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
/* 線形リスト用構造体 */
struct list {
int data; /* データ */
struct list *next_ptr; /* 次のノードへのポインタ */
};
typedef struct list List;
/* 関数宣言 */
List *test05_add_list(List *old_list, int data);
void test05_print_list(List *list);
void test05_free(List *list);
/* 線形リストへのデータ追加 */
List *test05_add_list(List *old_list, int data) {
List *new_list;
new_list = (List *)calloc(1, sizeof(List));
new_list->data = data;
new_list->next_ptr = old_list;
return new_list;
}
/* 線形リストのデータ表示 */
void test05_print_list(List *list) {
while(list != NULL) {
printf("%d ", list->data); /* データを表示 */
list = list->next_ptr; /* 次のノードへ */
}
printf("\n");
return;
}
/* 線形リストのメモリ解放 */
void test05_free(List *list) {
List *p, *q;
for (p = list; p != NULL; p = q) {
q = p->next_ptr;
free(p);
}
list = NULL;
return;
}
#endif
Nim側はソース名をsample5.nimに設定し、以下を記述。
また、C関数への引数は、object型で指定した構造体を設定します。object型に(struct list)を指定し、そのポインタを引数に渡します。
# C言語のソースファイル名を指定
from std/os import parentDir, `/`
const clangHeader = currentSourcePath().parentDir()/"csrc/clang_sample5.h"
# 線形リストの構造体を受け渡し
type
ListObj {.bycopy, header: clangHeader, importc: "struct list".} = object
data {.importc: "data".}: cint
next_ptr {.importc: "next_ptr".}: List
List = ptr ListObj
# 関数定義 (連携する関数名を定義)
proc test05_add_list(p: List, d: cint): List {.header: clangHeader, importc:"test05_add_list".}
proc test05_print_list(p: List) {.header: clangHeader, importc:"test05_print_list".}
proc test05_free(p: List) {.header: clangHeader, importc:"test05_free".}
# int配列を作成 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]となるように設定
var ar: array[10, int32]
for i in countup(0, 9):
ar[i] = cast[int32](i)
# データを線形リストに登録
var list: List = nil
for i in countup(0, 9):
list = test05_add_list(list, ar[i])
# 線形リストの結果表示
test05_print_list(list)
# 線形リストのメモリ解放
test05_free(list)
実行結果
$ nim r --hints:off .\sample5.nim
9 8 7 6 5 4 3 2 1 0
プログラムの説明
C言語側で毎回データを登録する時に、メモリを確保し連結させているので、C言語側で、再度メモリを解放してあげます。
object型の構造体に対して、bycopyプラグマ(値渡し指定)を付けていますが、無くても動作します。参考にした資料(nimraylibのソース)が付けていたので同じようにしてみました。
C++言語の関数を呼び出す場合
1. NimからC++のクラスを呼び出す
C言語のソースをcsrc/cpplang_sample.hppとして以下に記述します。
#ifndef CPPLANG_SAMPLE_HPP
#define CPPLANG_SAMPLE_HPP
using namespace std; // std::coutを省略するため
#include <iostream>
class Person {
public:
Person(string n, int a): name(n), age(a) {
cout << "コンストラクタ" << endl;
}
void addAge(int b) {
age = age + b;
}
int getAge() {
return age;
}
char *getName() {
return (char *)name.c_str();
}
~Person() {
cout << "デストラクタ" << endl;
}
private:
string name;
int age;
};
#endif
Nim側はソース名をcpplang_sample.nimに設定し、以下を記述。
また、C++関数への引数は、Personクラス(object PersonObjのポインタ)を設定します。
#[
C++Classの呼び出し
nim r -b:cpp .\cpplang_sample.nim
cdeclプラグマ(呼び出し規約をcdeclに準拠)を使えば、-b:cppは不要
]#
from std/os import parentDir, `/`
const cppHeader = currentSourcePath().parentDir()/"csrc/cpplang_sample.hpp"
type
PersonObj {.header:cppHeader, importcpp: "Person".} = object
Person = ptr PersonObj
proc newPerson(s:cstring, x: cint): Person {.header:cppHeader, importcpp: "new Person(@)".}
proc addAge(this: Person, x: cint) {.header:cppHeader, importcpp: "#.addAge(@)".} # "#." はクラスポインタ参照型の ->の意味
proc getAge(this: Person): int {.header:cppHeader, importcpp: "#.getAge(@)".}
proc getName(this: Person): cstring {.header:cppHeader, importcpp: "#.getName(@)".}
proc destroy(this: Person) {.header:cppHeader, importcpp: "#.~Person(@)".}
when isMainModule:
var mi = newPerson("sample", 5)
mi.addAge(2)
echo mi.getName()
echo mi.getAge()
mi.destroy()
実行結果
$ nim r -b:cpp --hints:off .\cpplang_sample.nim
コンストラクタ
sample
7
デストラクタ
プログラムの説明
C++のクラスを呼び出す場合は、コンパイルに-b:cppまたはNimソースにcdeclを追加する必要があります。
クラス内の値に、int型とstring型の値を設定し、それぞれ値を渡します。
getNameでstd::string型を返そうとしたが、エラーになるので、char *型に変換します。(何故エラーになるのはは、わかりませんが、char *型で戻せば動作可能。)
2. NimからC++のSTLを呼び出す
STL(Standard Template Libraryの略)はC++言語の標準ライブラリの事です。それをダイレクトにNim側で呼び出す事が可能ですが、NimからC++への制約(ルール)が多いので、私も細かくわかっていません。
例えるなら、Binding Nim to C++ std::listの記事を読まれた方がわかりやすいかもしれません。
この記事では、STLのListライブラリのみを呼び出して、利用する例が載っています。
また、STL全般のライブラリをバインドしたライブラリも別途あるので、参照されても良いかもしれません。
Nim bindings for the C++ STL
Discussion