asopi tech
asopi techIndie Developer

ASMATI・TATSUMIEXPERIMENT NOTE 01

Nimのintとfloatを生成Cの具体型まで確認する

公開日: 2026/08/25読了時間: 約 12分

アスマティ・辰巳「高水準言語は低水準で何をしているのか」第1回

本シリーズでは、NimコンパイラがNimのプログラムをCコードへ変換した結果を、生成されたCコードで確認します。第1回では、三つの関数(proc)をコンパイルし、intfloatの具体的なC型と、加算・除算の実装を解説します。

1. 前提条件

ビルドモードはdebug(opt: none)です。実験条件は次のとおりです。

項目条件
OSmacOS 26.5.2
CPUApple Silicon
Nim2.2.10
CコンパイラApple clang 21.0.0
メモリ管理ORC
ビルドモードdebug(opt: none

2. 引数と戻り値を変えた三つの関数

ソースコードには、引数と戻り値が異なる三つの小さなprocを定義しました。生成Cの関数宣言と計算処理を、この三つで比較します。

scalar_proc.nim
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は引数を取らず、int7を返します。関数addOneintの引数へ1を加えてintを返します。関数halffloatの引数を2.0で割ってfloatを返します。引数と戻り値には整数または浮動小数点数だけを使っているため、生成された三つのC関数を型と計算処理で比較できます。

when isMainModuleは、このファイルをメインプログラムとしてコンパイルした場合だけ、ブロック内のコードを含める指定です。isMainModuleNim 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コードのうち、三つの関数宣言と関数本体は次のとおりです。

generated_c_excerpt.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_PRIVATEN_NIMCALLNIが実際にどのCコードへ展開されるかは決まりません。これらのマクロと型名は、後に示すnimbase.hの定義と合わせて確認します。

3. nimbase.hで決まるNIとNFの具体型

生成された三つの関数宣言は、intfloatの代わりに、NimのCバックエンドが定義する型名NINFを使っています。具体的なC型は、生成CのNIM_INTBITSNim 2.2.10のlib/nimbase.hにあるNINFN_LIB_PRIVATEN_NIMCALLの定義で決まります。今回選ばれた定義は次のとおりです。

nimbase.h
#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分岐が選ばれ、NI64int64_tとして定義されます。生成CのNIM_INTBITS 64NI64を選ぶため、NIの具体型はint64_tです。NFdoubleとして定義されています。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になります。さらにNIint64_tとして解決されます。

他の二つの関数に対しても同様に定義を適用すると、関数addOneの引数と戻り値はint64_t、関数halfの引数と戻り値はdoubleです。

4. 具体型を解決した関数本体

NIint64_tNFdoubleであり、以下の関数本体ではこれらの具体型として扱われます。関数noArgsの定義は次の部分です。

N_LIB_PRIVATE N_NIMCALL(NI, noArgs__scalar95proc_u1)(void) {
  NI result;
  result = ((NI)7);
  return result;
}

先頭は宣言と同じ関数シグネチャであり、{から}までが関数本体です。NI result;は、戻り値を保持する局所変数resultNI型で宣言します。このNIint64_tです。result = ((NI)7);は、整数リテラル7int64_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を受け取ります。マクロnimAddIntx_p01を加算し、結果を局所変数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_p02.0NFへ型変換し、Cの除算演算子で計算した値をresultへ代入して返します。

5. int加算に入るoverflow検査

関数addOneの本体で呼ばれるマクロnimAddIntは、Nim 2.2.10のlib/nimbase.hで定義されています。今回の条件で選ばれる定義を示します。

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

関数raiseOverflowNim 2.2.10のlib/system/integerops.nimcompilerprocとして定義されています。compilerprocは、コンパイラが生成コードから呼び出す関数を指定するpragmaです。raiseOverflowはoverflow時にOverflowDefectを指定してsysFatalを呼びます。

integerops.nim
proc raiseOverflow {.compilerproc, noinline.} =
  sysFatal(OverflowDefect, "over- or underflow")

マクロnimAddIntが展開される__builtin_saddll_overflowは、加算結果を第3引数へ書き、overflowを検出すると真を返します。関数addOneから生成されたC関数は、その戻り値が真のときに関数raiseOverflow()を呼びます。

今回のdebug buildでは、Nim runtimeのnimAddIntraiseOverflow()int加算のoverflowを検査します。関数halffloat除算にはruntime関数が入らず、生成CがCの除算演算子を実行します。

6. 実行結果

生成したバイナリは、三つの関数について次の値を返しました。

noArgs=7
addOne(41)=42
half(3.5)=1.75

実行結果には、三つの関数が返した値が現れます。関数シグネチャ、整数幅、overflow検査は、前章までに掲載した生成Cで確認できます。

Nimのソースコードをコンパイルし、生成したバイナリを実行しています。

動画を開く

動画では、Nimのソースコードをコンパイルし、生成したバイナリを直接実行しています。読者は動画内の二つのコマンドを同じ順序で実行し、三つの関数の戻り値を確認できます。

7. Nimのintとfloatに対応するC型

今回のMac(Apple Silicon)環境では、NimのintはCのint64_tへ、floatdoubleへ変換されました。intの型幅は環境に応じてマクロで選ばれ、debug buildの整数加算にはoverflow検査が自動的に差し込まれます。

ソースコード、生成Cの抜粋、実行結果、検証スクリプトは、asmati-labのexperiment 004にあります。

参考リンク