ASMATI・TATSUMIEXPERIMENT NOTE 02
Nimの関数をCの公開APIとして呼び出す
アスマティ・辰巳「高水準言語は低水準で何をしているのか」第2回
本シリーズでは、NimコンパイラがNimのプログラムをCコードへ変換した結果を、生成されたCコードで確認します。第2回では、二つの関数(proc)をC向けの動的ライブラリとしてコンパイルし、生成ヘッダーを使うCプログラムから呼び出し、その手順に沿って公開シンボルとNimの初期化処理を確認します。
1. 前提条件
今回は、NimからmacOSのdynamic libraryを作り、独立したCプログラムから二つの関数を呼び出します。実験条件は次のとおりです。
| 項目 | 条件 |
|---|---|
| OS | macOS 26.5.2 |
| CPU | Apple Silicon |
| Nim | 2.2.10 |
| Cコンパイラ | Apple clang 21.0.0 |
| メモリ管理 | ORC |
| ビルドモード | debug(opt: none) |
| 利用側のCソース | C11 |
2. C向けに公開する二つの関数
NimのcintはCのintに、cdoubleはCのdoubleに対応する型です。Nim 2.2.10のsystem/ctypesでは、cint = int32、cdouble = float64と定義しています。
今回は、cintを二つ引数に取る関数と、cdoubleを一つ引数に取る関数(proc)をNim側に定義します。
proc asmati_add_ints(left, right: cint): cint {.exportc, dynlib, cdecl.} =
left + right
proc asmati_half(value: cdouble): cdouble {.exportc, dynlib, cdecl.} =
value / 2.0関数asmati_add_intsは、引数leftとrightを加算し、その整数をcintで返します。関数asmati_halfは、引数valueを2.0で割り、半分にした値をcdoubleで返します。引数leftとrightの型はcint、引数valueの型はcdoubleです。
Nim 2.2.10 Manualのexportc pragmaは、関数をCから利用できる名前で出力します。dynamic libraryへ関数を公開する場合は、dynlibをexportcと組み合わせます。cdecl pragmaは、Cコンパイラと同じ呼び出し規約を指定します。
3. コンパイルで生成されるファイル
次の動画では、Nimのソースコードから生成C、Cヘッダー、dynamic libraryを作ります。
動画で実行したコマンドでは、次の引数に対応するファイルが生成されます。
--nimcache:observed/nimcache
生成ファイルはobserved/nimcache/@mscalar_api.nim.cとobject fileです。前者はNimのソースコードから生成されたCコードであり、後者はそのCコードをコンパイルした結果です。
--header:scalar_api.h
生成ファイルはobserved/nimcache/scalar_api.hです。Cから二つの関数を呼ぶための宣言が含まれます。
--out:observed/bin/libscalar_api.dylib
生成ファイルはobserved/bin/libscalar_api.dylibです。コンパイル済みのobject fileをリンクしたdynamic libraryです。
scalar_api.hには、次の宣言が含まれます。実験repositoryでは、確認しやすいように同じ内容をobserved/scalar_api.hへ保存しています。
#define NIM_INTBITS 64
#include "nimbase.h"
N_LIB_IMPORT N_CDECL(int, asmati_add_ints)(int left_p0, int right_p1);
N_LIB_IMPORT N_CDECL(double, asmati_half)(double value_p0);
N_LIB_IMPORT N_CDECL(void, NimMain)(void);関数asmati_add_intsの引数と戻り値はCのintです。関数asmati_halfの引数と戻り値はCのdoubleです。第1回で確認したNimのintとfloatではなく、今回はNim側でcintとcdoubleを指定したため、生成ヘッダーがCの型を直接使います。
生成されたscalar_api.hは、Nim 2.2.10に同梱されたランタイム用ヘッダーlib/nimbase.hを読み込みます。生成ヘッダーを使うCソースをコンパイルするときは、生成ヘッダーのディレクトリに加えて、Nim 2.2.10のlibディレクトリをinclude pathへ指定します。
4. dynamic libraryの元になる生成C
observed/nimcache/@mscalar_api.nim.cは、Nimコンパイラがsrc/scalar_api.nimから生成したCコードです。このCコードをコンパイルしたobject fileが、libscalar_api.dylibへリンクされます。
次のコードは@mscalar_api.nim.cから二つの関数を抜粋したものです。実験repositoryのobserved/generated_c_excerpt.cは、この抜粋を検証用に保存したテキストファイルであり、dynamic libraryではありません。
N_LIB_EXPORT N_CDECL(int, asmati_add_ints)(int left_p0, int right_p1) {
int result;
NI temporary;
result = (int)0;
if (nimAddInt(left_p0, right_p1, &temporary)) {
raiseOverflow();
goto BeforeRet_;
}
result = (int)(temporary);
BeforeRet_:
return result;
}
N_LIB_EXPORT N_CDECL(double, asmati_half)(double value_p0) {
double result;
result = ((NF)(value_p0) / (NF)(2.0));
return result;
}関数asmati_add_intsの公開シグネチャは、二つのintを受け取ってintを返します。関数本体では、nimAddIntが加算結果をNI型の局所変数temporaryへ書き込み、オーバーフローを検出した場合はraiseOverflowを呼びます。最後にtemporaryをintへキャストして返します。
関数asmati_halfの公開シグネチャは、doubleを一つ受け取ってdoubleを返します。関数本体では、引数value_p0と2.0をNFへキャストして除算し、結果をdouble型の局所変数resultへ代入します。NIとNFはNimランタイムの型別名であり、今回の環境ではそれぞれint64_tとdoubleへ展開されます。
Nim 2.2.10に同梱されたlib/nimbase.hでは、C11とApple clangの条件に対して次の定義が選ばれます。
#ifdef __cplusplus
# define NIM_EXTERNC extern "C"
#else
# define NIM_EXTERNC
#endif
#define N_CDECL(rettype, name) rettype name
#define N_LIB_EXPORT NIM_EXTERNC __attribute__((visibility("default")))N_LIB_EXPORTは、C11では__attribute__((visibility("default")))へ展開されます。N_CDECL(int, asmati_add_ints)はint asmati_add_intsになり、関数asmati_add_intsはdefault visibilityを持つC関数として定義されます。C++で同じヘッダーを使う場合は、NIM_EXTERNCがextern "C"を加えます。
5. dynamic libraryの公開シンボル
生成したlibscalar_api.dylibの公開シンボルを確認します。
nm -gU observed/bin/libscalar_api.dylib実行結果には、二つの関数とNimの初期化関数が含まれます。
000000000000cc08 T _NimDestroyGlobals
000000000000cc40 T _NimMain
000000000000c9f8 T _asmati_add_ints
000000000000cba4 T _asmati_halfMach-OのnmはCシンボルの先頭へunderscoreを付けて表示します。_asmati_add_intsと_asmati_halfは、生成Cの関数名asmati_add_intsとasmati_halfに対応します。ヘッダーの宣言とdynamic libraryの公開シンボルが同じ二つの関数を示しています。
6. library constructorによるNimの初期化
Cプログラムから二つの関数を呼び出す前に、Nim runtimeがどこで初期化されるかを確認します。生成Cには、NimMainを呼ぶ関数NimMainInitが含まれます。
N_LIB_EXPORT N_CDECL(void, NimMain)(void) {
PreMain();
NimMainInner();
}
N_LIB_PRIVATE void NIM_POSIX_INIT NimMainInit(void) {
NimMain();
}関数NimMainはPreMain()とNimMainInner()を呼び、Nim runtimeとモジュールを初期化します。関数NimMainInitは、そのNimMain()を関数本体から呼びます。
NIM_POSIX_INITは、Nim 2.2.10に同梱されたlib/nimbase.hで次のattributeとして定義されています。
#define NIM_POSIX_INIT __attribute__((constructor))constructor function attributeを持つ関数は、プログラムがmainへ入る前に自動実行されます。今回の生成物では、dynamic libraryの関数NimMainInitがmainより前に実行され、その関数がNimMainを呼びます。
Nim Backend Integration 2.2.10は、CやC++からNimのコードを使う前にNimMainによる初期化が必要であることを説明しています。今回のmacOS/POSIX向けdynamic libraryでは、その初期化呼び出しが生成されたlibrary constructorに置かれています。
7. 呼び出し側の実装
生成ヘッダーを使う呼び出し側を、src/caller.cに実装します。このCプログラムは、二つの公開関数を呼び出します。
#include <stdio.h>
#include "scalar_api.h"
int main(void) {
const int sum = asmati_add_ints(19, 23);
const double half = asmati_half(3.5);
printf("add(19,23)=%d\n", sum);
printf("half(3.5)=%.2f\n", half);
if (sum != 42 || half != 1.75) {
return 1;
}
return 0;
}#include <stdio.h>は、実行結果の表示に使う関数printfの宣言を取り込みます。#include "scalar_api.h"は、Nimコンパイラが生成したヘッダーから関数asmati_add_intsと関数asmati_halfの宣言を取り込みます。呼び出し側は、二つの関数宣言をcaller.cへ書き写さず、生成ヘッダーを参照します。
局所変数sumは関数asmati_add_intsの戻り値を受け取り、局所変数halfは関数asmati_halfの戻り値を受け取ります。関数mainにはNimMainの呼び出しを記述していません。
次のclangコマンドは、src/caller.cをコンパイルし、dynamic libraryとリンクして、実行ファイルobserved/bin/c_callerを生成します。続くコマンドは、その実行ファイルを起動します。
clang -std=c11 -Wall -Wextra -Werror \
-Iobserved -I<NIM_LIB> \
src/caller.c \
-Lobserved/bin -lscalar_api \
-Wl,-rpath,@loader_path \
-o observed/bin/c_caller
./observed/bin/c_caller実行結果は次のとおりです。
add(19,23)=42
half(3.5)=1.75最後に、実行ファイルが外部へ要求する未解決シンボルをnm -uで確認します。
nm -u observed/bin/c_caller_asmati_add_ints
_asmati_half
_printf実行ファイルobserved/bin/c_callerは、関数asmati_add_intsと関数asmati_halfをdynamic libraryへ要求しています。出力に_NimMainはなく、呼び出し側はNimMainを直接参照していません。前章で確認したlibrary constructorがNimMainを呼び、Nim runtimeを初期化しています。
8. Cの公開APIとして確認する範囲
Nimの関数をCの公開APIとして使う場合、生成ヘッダーがC向けの関数宣言を持ち、libraryが対応する公開シンボルを提供します。利用側が実装したCソースは、そのヘッダーとlibraryを使ってコンパイル・リンクでき、Nim runtimeの初期化を完了した後に関数を実行します。
ここまでの手順で、exportc、dynlib、cdeclを指定した二つの関数について、Nim側の定義からCプログラムの実行結果までを確認できます。Nimのソースコード、呼び出し側のCソース、生成ヘッダー、生成Cの抜粋、公開シンボル、リンク情報、実行結果、検証スクリプトは、asmati-labのexperiment 006にあります。