🐥

NimからCの関数を呼び出す

2024/06/03に公開

久しぶりの投稿になります。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度読みさせないようにします。

csrc/clang_sample1.h
#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プラグマの後に関数名を書きますが、関数名が同じなら無くても問題ありません。

sample1.nim
# 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として以下に記述します。

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型を設定します。

sample2.nim
# 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として以下に記述します。

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型を設定します。

sample3.nim
# 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として以下に記述します。

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型を設定します。

sample4.nim
# 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として以下に記述します。

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)を指定し、そのポインタを引数に渡します。

sample5.nim
# 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として以下に記述します。

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のポインタ)を設定します。

cpplang_sample.nim
#[
  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