ASMATI・TATSUMIEXPERIMENT NOTE 01
Nimのintとfloatを生成Cの具体型まで確認する
アスマティ・辰巳「高水準言語は低水準で何をしているのか」第1回
本シリーズでは、NimコンパイラがNimのプログラムをCコードへ変換した結果を、生成されたCコードで確認します。第1回では、三つの関数(proc)をコンパイルし、intとfloatの具体的なC型と、加算・除算の実装を解説します。
1. 前提条件
ビルドモードはdebug(opt: none)です。実験条件は次のとおりです。
| 項目 | 条件 |
|---|---|
| OS | macOS 26.5.2 |
| CPU | Apple Silicon |
| Nim | 2.2.10 |
| Cコンパイラ | Apple clang 21.0.0 |
| メモリ管理 | ORC |
| ビルドモード | debug(opt: none) |
2. 引数と戻り値を変えた三つの関数
ソースコードには、引数と戻り値が異なる三つの小さなprocを定義しました。生成Cの関数宣言と計算処理を、この三つで比較します。
proc noArgs(): int =
7
proc addOne(x: int): int =
x + 1
proc half(x: float): float =
x / 2.0
when isMainModule:
echo "noArgs=", noArgs()
echo "addOne(41)=", addOne(41)
echo "half(3.5)=", half(3.5)関数noArgsは引数を取らず、intの7を返します。関数addOneはintの引数へ1を加えてintを返します。関数halfはfloatの引数を2.0で割ってfloatを返します。引数と戻り値には整数または浮動小数点数だけを使っているため、生成された三つのC関数を型と計算処理で比較できます。
when isMainModuleは、このファイルをメインプログラムとしてコンパイルした場合だけ、ブロック内のコードを含める指定です。isMainModuleはNim 2.2.10のsystemモジュールで定義されているbool型の定数で、メインモジュールから参照した場合だけ真になります。また、whenはコンパイル時の条件分岐であり、このブロックはコンパイル時に解決されます。
このブロックは、三つのprocを具体的な値で呼び出し、結果を標準出力へ表示します。同じソースコードから生成Cを取得し、コンパイルしたバイナリの戻り値も確認できます。
Nim Tutorial Part IのModulesでは、モジュールをメインファイルとしてコンパイルした場合だけテストコードを含める例でwhen isMainModuleを使っています。
次のコマンドでsrc/scalar_proc.nimをコンパイルしました。Nimコンパイラが出力したCコードはobserved/nimcacheへ保存されます。
nim c --nimcache:observed/nimcache \
-o:observed/bin/scalar_proc \
src/scalar_proc.nim生成されたCコードのうち、三つの関数宣言と関数本体は次のとおりです。
#define NIM_INTBITS 64
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void);
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0);
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0);
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) {
NI result;
result = ((NI)7);
return result;
}
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0) {
NI result;
NI tmp;
result = (NI)0;
if (nimAddInt(x_p0, ((NI)1), &tmp)) {
raiseOverflow();
goto BeforeRet_;
}
result = (NI)(tmp);
BeforeRet_:
return result;
}
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0) {
NF result;
result = ((NF)(x_p0) / (NF)(2.0));
return result;
}この抜粋には、関数シグネチャと戻り値を計算して返すコードだけを掲載しています。debug用のnimfr_、nimlf_、popFrame()は省略しました。
最初の#define NIM_INTBITS 64は、この生成CがNimのintを64bit幅で扱うことを指定しています。続く関数noArgsの宣言を詳しく解説します。
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void);この行は、Nimの関数noArgsから生成されたC関数noArgs__scalar95proc_u1の宣言です。N_NIMCALL(NI, noArgs__scalar95proc_u1)(void)は、関数の宣言を生成するマクロを使った表記です。宣言上の戻り値はNIで、(void)は引数がないことを示します。行末は;であり、この行は関数の宣言のみとなっています。
この宣言だけでは、N_LIB_PRIVATE、N_NIMCALL、NIが実際にどのCコードへ展開されるかは決まりません。これらのマクロと型名は、後に示すnimbase.hの定義と合わせて確認します。
3. nimbase.hで決まるNIとNFの具体型
生成された三つの関数宣言は、intとfloatの代わりに、NimのCバックエンドが定義する型名NIとNFを使っています。具体的なC型は、生成CのNIM_INTBITSとNim 2.2.10のlib/nimbase.hにあるNI、NF、N_LIB_PRIVATE、N_NIMCALLの定義で決まります。今回選ばれた定義は次のとおりです。
#define N_LIB_PRIVATE __attribute__((visibility("hidden")))
#define N_NIMCALL(rettype, name) rettype name /* no modifier */
/* ... */
typedef int64_t NI64;
/* ... */
#ifdef NIM_INTBITS
# if NIM_INTBITS == 64
typedef NI64 NI;
typedef NU64 NU;
# elif NIM_INTBITS == 32
typedef NI32 NI;
typedef NU32 NU;
# elif NIM_INTBITS == 16
typedef NI16 NI;
typedef NU16 NU;
# elif NIM_INTBITS == 8
typedef NI8 NI;
typedef NU8 NU;
# else
# error "invalid bit width for int"
# endif
#endif
typedef double NF;Apple clangではC99向けのstdint.h分岐が選ばれ、NI64はint64_tとして定義されます。生成CのNIM_INTBITS 64はNI64を選ぶため、NIの具体型はint64_tです。NFはdoubleとして定義されています。macOSでは、N_LIB_PRIVATEが関数シンボルへhidden visibilityを指定し、N_NIMCALL(rettype, name)はrettype nameへ展開されます。
この定義を関数noArgsの宣言へ適用すると、宣言は次のCコードになります。
__attribute__((visibility("hidden")))
int64_t noArgs__scalar95proc_u1(void);N_LIB_PRIVATEは__attribute__((visibility("hidden")))になり、N_NIMCALL(NI, noArgs__scalar95proc_u1)はNI noArgs__scalar95proc_u1になります。さらにNIがint64_tとして解決されます。
他の二つの関数に対しても同様に定義を適用すると、関数addOneの引数と戻り値はint64_t、関数halfの引数と戻り値はdoubleです。
4. 具体型を解決した関数本体
NIはint64_t、NFはdoubleであり、以下の関数本体ではこれらの具体型として扱われます。関数noArgsの定義は次の部分です。
N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) {
NI result;
result = ((NI)7);
return result;
}先頭は宣言と同じ関数シグネチャであり、{から}までが関数本体です。NI result;は、戻り値を保持する局所変数resultをNI型で宣言します。このNIはint64_tです。result = ((NI)7);は、整数リテラル7をint64_tへ型変換してからresultへ代入します。return result;は、その値を呼び出し元へ返します。Nimのproc noArgs(): int = 7は、今回の生成Cでは、int64_t型の局所変数へ7を入れて返すC関数になっています。
次に、整数加算とoverflow時の分岐を含む関数addOneの本体を示します。
N_LIB_PRIVATE N_NIMCALL(NI, addOne__scalar95proc_u3)(NI x_p0) {
NI result;
NI tmp;
result = (NI)0;
if (nimAddInt(x_p0, ((NI)1), &tmp)) {
raiseOverflow();
goto BeforeRet_;
}
result = (NI)(tmp);
BeforeRet_:
return result;
}関数addOneから生成されたC関数は、int64_tへ解決されるNI型の引数x_p0を受け取ります。マクロnimAddIntはx_p0と1を加算し、結果を局所変数tmpへ書き込みます。overflowを検出した場合は関数raiseOverflow()を呼び、正常に加算できた場合はtmpの値をresultへ代入して返します。
関数halfでは、浮動小数点数の除算が生成Cでどの式になるかを確認します。
N_LIB_PRIVATE N_NIMCALL(NF, half__scalar95proc_u6)(NF x_p0) {
NF result;
result = ((NF)(x_p0) / (NF)(2.0));
return result;
}関数halfから生成されたC関数は、doubleへ解決されるNF型の引数x_p0を受け取ります。x_p0と2.0をNFへ型変換し、Cの除算演算子で計算した値をresultへ代入して返します。
5. int加算に入るoverflow検査
関数addOneの本体で呼ばれるマクロnimAddIntは、Nim 2.2.10のlib/nimbase.hで定義されています。今回の条件で選ばれる定義を示します。
#if (!defined(_MSC_VER) || defined(__clang__)) && \
!defined(NIM_EmulateOverflowChecks)
/* ... */
#if NIM_INTBITS == 32
/* ... */
#else
#define nimAddInt(a, b, res) \
__builtin_saddll_overflow(a, b, (long long int*)res)
#endif
#endif関数raiseOverflowはNim 2.2.10のlib/system/integerops.nimでcompilerprocとして定義されています。compilerprocは、コンパイラが生成コードから呼び出す関数を指定するpragmaです。raiseOverflowはoverflow時にOverflowDefectを指定してsysFatalを呼びます。
proc raiseOverflow {.compilerproc, noinline.} =
sysFatal(OverflowDefect, "over- or underflow")マクロnimAddIntが展開される__builtin_saddll_overflowは、加算結果を第3引数へ書き、overflowを検出すると真を返します。関数addOneから生成されたC関数は、その戻り値が真のときに関数raiseOverflow()を呼びます。
今回のdebug buildでは、Nim runtimeのnimAddIntとraiseOverflow()がint加算のoverflowを検査します。関数halfのfloat除算にはruntime関数が入らず、生成CがCの除算演算子を実行します。
6. 実行結果
生成したバイナリは、三つの関数について次の値を返しました。
noArgs=7
addOne(41)=42
half(3.5)=1.75実行結果には、三つの関数が返した値が現れます。関数シグネチャ、整数幅、overflow検査は、前章までに掲載した生成Cで確認できます。
動画では、Nimのソースコードをコンパイルし、生成したバイナリを直接実行しています。読者は動画内の二つのコマンドを同じ順序で実行し、三つの関数の戻り値を確認できます。
7. Nimのintとfloatに対応するC型
今回のMac(Apple Silicon)環境では、NimのintはCのint64_tへ、floatはdoubleへ変換されました。intの型幅は環境に応じてマクロで選ばれ、debug buildの整数加算にはoverflow検査が自動的に差し込まれます。
ソースコード、生成Cの抜粋、実行結果、検証スクリプトは、asmati-labのexperiment 004にあります。