Ada と C / C# をつなぐ ── DLL を作って呼び出す

· · Ada, GNAT, C, C#, .NET, P/Invoke, DLL

Xでポスト はてなブックマーク
更新履歴(初版のみ・2026年11月01日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.23855021)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Ada と C / C# をつなぐ ── DLL を作って呼び出す」合同会社小村ソフト. https://doi.org/10.5281/zenodo.23855021 https://comcomponent.com/blog/ada-c-csharp-dll-interop/

DOI(最新版)
10.5281/zenodo.23855021
DOI(この版)
10.5281/zenodo.23855022

Ada で書いた処理を、C や C# のアプリから使いたいことがあります。逆に、Ada のプログラムから Windows の API や既存の DLL を呼びたいこともあります。Ada は他の言語とのつなぎ方を参照マニュアルの附属書 B に定めているので、関数の宣言に Convention => C を付ければ、C と同じ呼び方の関数として公開したり呼び出したりできます。

ただし、関数を公開するだけでは足りません。Ada のライブラリには使う前に済ませる初期化(elaboration)があり、文字列の領域をどちらが確保して解放するかも決める必要があります。Ada の例外が境界の外へ出ると、呼び出し側のプロセスが終わることもあります。この記事では小さなライブラリを gprbuild で共有ライブラリにし、C と C# から呼ぶ例で、これらを順に説明します。最後に、Ada から Windows の DLL を呼ぶ例と、Windows の DLL として配るときに確かめることを扱います。

例はすべて実際にビルドして動かしています。Linux(Ubuntu 24.04)の GNAT 13.3.0 と gprbuild で共有ライブラリ(.so)を作り、C は gcc、C# は .NET 8 で呼びました。Windows 向けは Ubuntu の MinGW-w64 版 GNAT 13 で DLL と実行ファイルを作り、Wine 9.0 の上で動かしました。実際の Windows では動かしていないので、Windows に固有の点は GNAT と Microsoft の資料を出典として書き、どこまでが実測かを節ごとに書き分けます。Ada そのものの紹介は Ada言語の魅力 にあります。

Ada と C の境界で決めること

Ada の参照マニュアルの附属書 B は、ほかの言語のサブプログラムを呼ぶ Import と、Ada のサブプログラムをほかの言語から呼べるようにする Export を定めています1。どの言語の呼び方に合わせるかは Convention で指定し、C に合わせるなら Convention => C と書きます。リンクするときの名前は External_Name で決めます。

Ada と C と C# の境界Ada のライブラリが Convention => C で C の呼び方に合わせて関数を公開し、C は直接、C# は P/Invoke で同じ関数を呼び、Ada は Import で Windows の DLL を呼ぶことを示す。Export と Convention => CP/InvokeImportAda のライブラリC の呼び方の関数C のプログラムC# のプログラムWindows の DLL

図1: Ada は C の呼び方に合わせて関数を公開し、C と C# はそれを呼ぶ。Ada から Windows の DLL を呼ぶときは Import を使う。

引数と戻り値の型には、パッケージ Interfaces.C の型を使います。int、size_t、char_array などは同じ名前の C の型に対応すると定められています2。Ada の Integer や String は、C の型と同じ表現になるとは定められていないので、境界にそのまま出しません。Ada の String は添字の範囲を持ち、C の文字列のような終端の NUL を持ちません。C の char * は Interfaces.C.Strings の chars_ptr で受けます3。

引数の渡し方にも決まりがあります。参照マニュアルは in のスカラーの引数を C の値渡しに、スカラー型などの基本の型(elementary type)の out と in out の引数をポインター(t*)に、配列の引数を要素へのポインターに対応させるよう勧めています2。この記事の例では C の int *result を Ada の Result : out int で受けます。

この記事の例で使う C と Ada の型の対応C の int、size_t、char *、int * を、Ada ではそれぞれ Interfaces.C の int、size_t、chars_ptr、out int の引数で受けることを示す。C の intAda の intC の size_tAda の size_tC の char *Ada の chars_ptrC の int *Ada の out int の引数

図2: この記事の例では C の型を Interfaces.C の同じ名前の型と chars_ptr で受け、int * は out の引数で受ける。

境界で決めることは、型のほかに3つあります。ライブラリの初期化を誰がいつ行うか、文字列やバッファーの領域をどちらが確保して解放するか、Ada の例外をどう扱うかです。以下の例で、それぞれの決め方を示します。

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全15件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

Ada の関数を C から呼べる形で書く

例として、足し算、割り算、あいさつ文の作成、呼び出し回数の数え上げの4つの関数を持つパッケージ Greet_API を作ります。仕様部(.ads)で、公開する関数に Export、Convention => C、External_Name を付けます。

with System;
with Interfaces.C;         use Interfaces.C;
with Interfaces.C.Strings; use Interfaces.C.Strings;

package Greet_API is

   --  int greet_add (int a, int b);
   function Add (A, B : int) return int
     with Export, Convention => C, External_Name => "greet_add";

   --  int greet_divide (int a, int b, int *result);
   --  戻り値: 0 = 成功、1 = 0 で割ろうとした、2 = 結果が int に収まらない
   function Divide (A, B : int; Result : out int) return int
     with Export, Convention => C, External_Name => "greet_divide";

   --  size_t greet_format (const char *name, char *buffer, size_t length);
   --  "Hello, <name>!" に必要な長さ (終端の NUL を含む) を返す。
   --  buffer が NULL でなく length が足りるときだけ、buffer に書き込む。
   function Format
     (Name   : chars_ptr;
      Buffer : System.Address;
      Length : size_t) return size_t
     with Export, Convention => C, External_Name => "greet_format";

   --  int greet_counter (void);  呼ばれるたびに 1 増やした値を返す
   function Counter return int
     with Export, Convention => C, External_Name => "greet_counter";

end Greet_API;

本体(.adb)は次のとおりです。Divide は例外を関数の中で処理してエラーコードを返し、Format は呼び出し側が渡した領域に文字列を書きます。Counter が数える変数は、パッケージ本体の末尾の begin から後ろ、つまり初期化の処理で 100 に設定しています。この値はあとで初期化が行われたかを見分けるのに使います。

with System; use type System.Address;

package body Greet_API is

   --  パッケージの初期化 (elaboration) で 100 に設定する
   Calls : int := 0;

   function Add (A, B : int) return int is
   begin
      return A + B;
   end Add;

   function Divide (A, B : int; Result : out int) return int is
   begin
      Result := 0;
      Result := A / B;
      return 0;
   exception
      when Constraint_Error =>
         if B = 0 then
            return 1;
         else
            return 2;
         end if;
   end Divide;

   function Format
     (Name   : chars_ptr;
      Buffer : System.Address;
      Length : size_t) return size_t
   is
   begin
      if Name = Null_Ptr then
         return 0;
      end if;

      declare
         Text : constant char_array := To_C ("Hello, " & Value (Name) & "!");
      begin
         if Buffer /= System.Null_Address and then Length >= Text'Length then
            declare
               --  呼び出し側の領域を、Text と同じ長さの char_array として扱う
               Target : char_array (Text'Range)
                 with Import, Address => Buffer;
            begin
               Target := Text;
            end;
         end if;
         return Text'Length;
      end;
   exception
      when others =>
         return 0;  --  例外を C 側へ出さない
   end Format;

   function Counter return int is
   begin
      Calls := Calls + 1;
      return Calls;
   end Counter;

begin
   Calls := 100;
end Greet_API;
greet_format の処理の流れname が NULL なら 0 を返し、そうでなければ文字列を作り、buffer が NULL でなく長さが足りるときだけ書き込んでから、必要な長さを返すことを示す。NULLNULL でない足りる足りないname は NULL か0 を返すHello, name! を作るbuffer があり長さが足りるかbuffer に書き込む書き込まない必要な長さを返す

図3: greet_format は、領域が足りるときだけ書き込み、いつも必要な長さを返す。

Format は、呼び出し側の領域を Address で Ada の配列に重ねて書き込みます。Interfaces.C.Strings には chars_ptr の指す先を書き換える Update もあります。参照マニュアルによると、Update は Check => True のとき、書き込みが既存の文字列の長さを超えると Update_Error を出します。Check => False にして、True なら Update_Error になる書き込みをすると、誤った実行(erroneous)になります3。呼び出し側が確保したばかりの領域にはまだ文字列が入っていないので、ここでは Update を使っていません。

gprbuild で共有ライブラリにする

共有ライブラリは gprbuild のプロジェクトファイルで作ります。Library_Kind を "dynamic" にすると、実行時に読み込む独立したライブラリになります4。Library_Interface に外へ見せるユニットを並べると、スタンドアロンライブラリ(Stand-alone Library)になります。スタンドアロンライブラリは Ada 以外の言語のプログラムから Ada の部分を使うのに向いた形です4。

library project Greet is
   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Library_Name use "greet";
   for Library_Dir use "lib";
   for Library_Kind use "dynamic";
   for Library_Interface use ("Greet_API");
   for Library_Auto_Init use "true";
end Greet;

gprbuild -p -P greet.gpr でビルドすると、lib/libgreet.so ができます。

Setup
   [mkdir]        object directory for project Greet
   [mkdir]        library directory for project Greet
Compile
   [Ada]          greet_api.adb
Build Libraries
   [gprlib]       greet.lexch
   [bind SAL]     greet
   [Ada]          b__greet.adb
   [link library] libgreet.so

nm -D で公開されている名前を見ると、External_Name で付けた4つの名前のほかに、greetinit と greetfinal があります。これはバインダーがライブラリ名から作る初期化と終了処理の手続きです4。

000000000000233a T greet_add@@SYMS
00000000000028ba T greet_api___elabb@@SYMS
000000000000287e T greet_counter@@SYMS
0000000000002380 T greet_divide@@SYMS
0000000000002460 T greet_format@@SYMS
00000000000029c0 T greetfinal@@SYMS
00000000000029e4 T greetinit@@SYMS
gprbuild でスタンドアロンライブラリを作る流れAda のソースをコンパイルし、バインダーが初期化の手続きを含むファイルを作り、それらを共有ライブラリにリンクすることを示す。コンパイルバインドgreet_api.ads と .adbオブジェクトファイル初期化の手続き(greetinit)リンクlibgreet.so(Windows では libgreet.dll)

図4: gprbuild はコンパイル、バインド、リンクを行い、初期化の手続きを含む共有ライブラリを作る。

このライブラリは GNAT のランタイムに依存します。ldd lib/libgreet.so の結果には libgnat-13.so が含まれていました。別のマシンへ配るときは、このランタイムも一緒に用意します。GPRbuild の資料にはランタイムまで取り込んで依存をなくす Library_Standalone use "encapsulated" もあります4。ただし、この環境(Ubuntu の GNAT 13)で試したところ、encapsulated にしても ldd の結果に libgnat-13.so が残りました。配る前に、手元のツールチェーンで依存を確かめてください。

初期化(elaboration)を誰が行うか

Ada のパッケージは、使う前に初期化(elaboration)が要ります5。パッケージ本体の begin から後ろの文や、変数の初期値の計算がこれに当たります。Ada のメインプログラムなら、バインダーが作るコードが起動時に行います。C や C# が呼ぶライブラリでは、ライブラリの側で行う必要があります。

Library_Auto_Init を指定しないか "true" にすると、自動の初期化に対応したプラットフォームでは、ライブラリを読み込んだときに初期化が行われます4。"false" にすると自動では行われず、ライブラリの機能を使う前に初期化の手続き(ここでは greetinit)を呼ぶ必要があります4。

違いを確かめるために、Counter だけを呼ぶ C のプログラムを、Library_Auto_Init を "true" にしたライブラリと "false" にしたライブラリにつないで動かしました。"true" では counter: 101、"false" では counter: 1 でした。"false" のほうでは、パッケージ本体の Calls := 100; が実行されていません。

counter: 1     (Library_Auto_Init = "false"、greetinit を呼ばない)
counter: 101   (Library_Auto_Init = "true")

初期化しないまま greet_format を呼んだ場合は、この環境ではセグメンテーション違反でプロセスが終わりました(原因までは調べていません)。初期化を忘れても毎回このように分かりやすく失敗するとは限らないので、自動にしない場合は呼び出し側で必ず初期化の手続きを呼びます。

#include <stdio.h>
#include <stdlib.h>

void   greetinit(void);
void   greetfinal(void);
size_t greet_format(const char *name, char *buffer, size_t length);
int    greet_counter(void);

int main(void)
{
    greetinit();   /* Ada 側の初期化 (elaboration) を明示的に行う */

    size_t needed = greet_format("Ada", NULL, 0);
    char *buffer = malloc(needed);
    greet_format("Ada", buffer, needed);
    printf("format: %s\n", buffer);
    free(buffer);
    printf("counter: %d\n", greet_counter());

    greetfinal();  /* 終了処理 (finalization) */
    return 0;
}

greetinit を呼んでから使うと、"false" のライブラリでも正しく動きました。

format: Hello, Ada!
counter: 101
Library_Auto_Init の設定と初期化の流れLibrary_Auto_Init が true ならライブラリの読み込み時に初期化が行われ、false なら呼び出し側が greetinit を呼んでから関数を使い、呼ばないとパッケージ本体の初期化が実行されないことを示す。truefalse呼んだ呼ばないLibrary_Auto_Init読み込み時に初期化greetinit を呼んだか初期化済み本体の初期化が実行されない関数を呼ぶ

図5: 自動の初期化を使わない場合は、呼び出し側が初期化の手続きを呼んでから関数を使う。

Windows では、自動の初期化に制約があります。GNAT の資料によると、Windows では DllMain の中でスレッドを作れないので、ライブラリのレベルで宣言したタスクはスタンドアロンライブラリでは使えません6。同じ資料には、ライブラリのレベルにタスクがあると DllMain から初期化を呼べないことも書かれています6。Ada のタスクについては Adaにおける安全な並行処理 で扱っています。

C から呼ぶ

C の側では Ada の仕様部に書いた C の宣言をそのまま関数の宣言として書き、ライブラリをリンクします。Library_Auto_Init を "true" にしたライブラリを使うので、greetinit は呼んでいません。

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

int    greet_add(int a, int b);
int    greet_divide(int a, int b, int *result);
size_t greet_format(const char *name, char *buffer, size_t length);
int    greet_counter(void);

int main(void)
{
    printf("add: %d\n", greet_add(2, 3));

    int q = 0;
    int rc = greet_divide(7, 2, &q);
    printf("divide 7/2: rc=%d q=%d\n", rc, q);
    printf("divide 1/0: rc=%d\n", greet_divide(1, 0, &q));
    printf("divide INT_MIN/-1: rc=%d\n", greet_divide(INT_MIN, -1, &q));

    /* 1回目で必要な長さを聞き、呼び出し側で確保してから2回目で書かせる */
    size_t needed = greet_format("Ada", NULL, 0);
    char *buffer = malloc(needed);
    greet_format("Ada", buffer, needed);
    printf("format: %s (needed=%zu)\n", buffer, needed);
    free(buffer);

    printf("counter: %d\n", greet_counter());
    printf("counter: %d\n", greet_counter());
    return 0;
}
gcc -Wall -o main main.c -L../greet/lib -lgreet
LD_LIBRARY_PATH=../greet/lib ./main

ビルドのときは -L と -lgreet でライブラリをリンクし、実行のときは LD_LIBRARY_PATH で libgreet.so のあるフォルダーを指定しています。

C のプログラムから Ada のライブラリを使う流れgcc で -lgreet を付けてリンクし、実行時に LD_LIBRARY_PATH のフォルダーから libgreet.so を読み込み、読み込み時に初期化が行われてから関数を呼ぶことを示す。gcc で -lgreet を付けてリンク実行するLD_LIBRARY_PATH から libgreet.so を読み込む読み込み時に初期化greet_add などを呼ぶ

図6: C のプログラムはリンクした libgreet.so を実行時に読み込み、初期化のあとで関数を呼ぶ。

実行結果です。0 で割ったときと、INT_MIN / -1 のように結果が int に収まらないときは、Ada の中で Constraint_Error になり、Divide の例外処理がそれぞれ 1 と 2 を返しました。counter が 101 から始まっているので、読み込み時に初期化が行われています。

add: 5
divide 7/2: rc=0 q=3
divide 1/0: rc=1
divide INT_MIN/-1: rc=2
format: Hello, Ada! (needed=12)
counter: 101
counter: 102

C# から呼ぶ

C# からは P/Invoke で呼びます。.NET 7 からは LibraryImport 属性が使え、マーシャリングのコードをコンパイル時に生成します7。従来の DllImport は実行時に IL スタブを生成します7。次の例は3つの関数を LibraryImport で、1つを DllImport で宣言しています。P/Invoke の書き方の全体は C#からWin32 APIを安全に呼ぶ にまとめています。

LibraryImport と DllImport のマーシャリングコードの作られ方LibraryImport はコンパイル時にマーシャリングのコードを生成し、DllImport は実行時に IL スタブを生成し、どちらも Ada のライブラリの関数を呼ぶことを示す。コンパイル時実行時LibraryImportマーシャリングのコードを生成DllImportIL スタブを生成Ada のライブラリの関数

図7: LibraryImport はコンパイル時に、DllImport は実行時に、呼び出しのためのコードを作る。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

Console.WriteLine($"add: {Greet.Add(2, 3)}");

int rc = Greet.Divide(7, 2, out int q);
Console.WriteLine($"divide 7/2: rc={rc} q={q}");
Console.WriteLine($"divide 1/0: rc={Greet.Divide(1, 0, out _)}");

// 1回目で必要な長さを聞き、C# 側で確保した配列に2回目で書かせる
nuint needed = Greet.Format("C#", null, 0);
byte[] buffer = new byte[(int)needed];
Greet.Format("C#", buffer, needed);
Console.WriteLine($"format: {Encoding.UTF8.GetString(buffer, 0, buffer.Length - 1)}");

Console.WriteLine($"counter: {Greet.CounterOld()}");

static partial class Greet
{
    // "greet" は Linux では libgreet.so、Windows では greet.dll として探される
    private const string Lib = "greet";

    [LibraryImport(Lib, EntryPoint = "greet_add")]
    [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
    internal static partial int Add(int a, int b);

    [LibraryImport(Lib, EntryPoint = "greet_divide")]
    [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
    internal static partial int Divide(int a, int b, out int result);

    [LibraryImport(Lib, EntryPoint = "greet_format",
                   StringMarshalling = StringMarshalling.Utf8)]
    [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
    internal static partial nuint Format(string name, byte[]? buffer, nuint length);

    // 従来の DllImport でも同じ関数を呼べる
    [DllImport(Lib, EntryPoint = "greet_counter",
               CallingConvention = CallingConvention.Cdecl)]
    internal static extern int CounterOld();
}

Ada 側は Convention => C で公開しているので、C# 側も呼び出し規約を Cdecl に合わせています。x86 の Windows では P/Invoke の既定が Stdcall なので、指定しないと呼び出し規約が食い違います。x64・ARM・ARM64 では呼び出し規約が1つしかなく、この指定は結果を変えません8。C の size_t は C# の nuint で受けています。string の引数は StringMarshalling.Utf8 を指定したので、UTF-8 の NUL 終端の文字列として Ada に渡ります。LibraryImport を使うプロジェクトでは AllowUnsafeBlocks を有効にする必要があります7。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>

AllowUnsafeBlocks を外してビルドすると、SYSLIB1062: LibraryImportAttribute requires unsafe code. というエラーになりました。有効にしてビルドし、ライブラリのフォルダーを LD_LIBRARY_PATH で指定して実行した結果です。

dotnet build -c Release
LD_LIBRARY_PATH=../greet/lib dotnet bin/Release/net8.0/cs.dll
add: 5
divide 7/2: rc=0 q=3
divide 1/0: rc=1
format: Hello, C#!
counter: 101

DllImport と LibraryImport に書いた "greet" は、ファイル名そのものではありません。.NET はプラットフォームに合わせて名前を変えながら探します。Linux と macOS では拡張子に加えて先頭に lib を付けた名前も試しますが、Windows では greet と greet.dll だけを試します9。一方、gprbuild で Windows 向けにビルドすると、あとで示すように libgreet.dll ができます。Windows で使うときは、"libgreet" と書くか、DLL の名前のほうを変える必要があります。

.NET がライブラリ名から探すファイル名DllImport に greet と書いたとき、Linux では greet.so と libgreet.so などを試し、Windows では greet と greet.dll だけを試すので、gprbuild が作る libgreet.dll は見つからないことを示す。LinuxWindowsDllImport(greet)実行する OSgreet.so や libgreet.so を試すgreet と greet.dll を試すlibgreet.so が見つかるlibgreet.dll は見つからないlibgreet と書く

図8: .NET は Windows では名前の先頭に lib を付けて探さないので、gprbuild が作る libgreet.dll には libgreet と書く。

文字列とメモリはどちらが確保してどちらが解放するか

境界を越える文字列やバッファーでは、領域をどちらが確保し、どちらが解放するかを決めます。領域は確保したときと同じ仕組みで解放する必要があります。

この記事の greet_format は、呼び出し側が領域を確保する形にしています。1回目は buffer に NULL を渡して必要な長さを受け取り、呼び出し側がその長さの領域を確保してから、2回目で書き込ませます。C は malloc と free、C# は byte[] で確保しており、Ada の側は書き込むだけです。確保と解放が同じ側で完結するので、ランタイムの違いを気にせずに済みます。

呼び出し側が領域を確保する文字列の受け渡し呼び出し側が buffer に NULL を渡して必要な長さを聞き、その長さを確保して再び呼び、Ada が書き込み、最後に呼び出し側が自分で解放することを示す。呼び出し側が NULL で呼ぶAda が必要な長さを返す呼び出し側が確保する呼び出し側が再び呼ぶAda が書き込む呼び出し側が解放する

図9: 呼び出し側が確保と解放を受け持ち、Ada は渡された領域に書き込むだけにする。

Ada の側で文字列を確保して返す形にするなら、解放する関数もライブラリから公開します。Interfaces.C.Strings の New_String で確保した領域は、C の関数ではなく Free で解放するよう参照マニュアルに書かれています3。New_String の実装は C の確保関数を使っても Ada の確保を使ってもよいとされているので、呼び出し側の free で解放してよいとは限りません。逆に、New_String と New_Char_Array 以外から得た chars_ptr を Free に渡すことも、誤った実行(erroneous)とされています3。

with Ada.Text_IO;          use Ada.Text_IO;
with Interfaces.C;         use Interfaces.C;
with Interfaces.C.Strings; use Interfaces.C.Strings;

procedure Use_Libc is

   --  size_t strlen (const char *s);
   function C_Strlen (S : chars_ptr) return size_t
     with Import, Convention => C, External_Name => "strlen";

   --  int abs (int j);
   function C_Abs (J : int) return int
     with Import, Convention => C, External_Name => "abs";

   S : chars_ptr := New_String ("Interfaces.C");
begin
   Put_Line ("strlen:" & size_t'Image (C_Strlen (S)));
   Put_Line ("abs:" & int'Image (C_Abs (-42)));
   Free (S);  --  New_String で確保した領域は Free で返す
end Use_Libc;

この例は Ada から C 標準ライブラリの strlen と abs を Import で呼んでいます。New_String で作った文字列を C の関数に渡し、使い終わったら Ada の Free で解放します。Linux で gnatmake use_libc.adb でビルドして実行すると、次のように出力されました。

strlen: 12
abs: 42

Ada のメモリ管理の全体は Adaのメモリ管理 で扱っています。

例外を境界の外へ出さない

Ada の関数の中で処理されなかった例外が、C から呼んだときにどうなるかを試しました。上の greet_add に INT_MAX と 1 を渡す C のプログラムを動かしました。Add は例外を処理していないので、Ada の中で Constraint_Error になります。

#include <limits.h>
#include <stdio.h>

int greet_add(int a, int b);

int main(void)
{
    printf("before\n");
    fflush(stdout);
    printf("after: %d\n", greet_add(INT_MAX, 1));
    return 0;
}
before

raised CONSTRAINT_ERROR : greet_api.adb:10 overflow check failed

GNAT のランタイムがメッセージを出し、プロセスは終了コード 1 で終わりました。after は出力されていません。C# から同じ関数を try と catch (Exception) で囲んで呼んでも、結果は同じでした。catch には入らず、同じメッセージを出してプロセスが終了しました。

C# の try の中で Ada の例外が起きたときの結果C# の try の中から greet_add を呼び、Ada の中で Constraint_Error が起きると、GNAT のランタイムがメッセージを出してプロセスが終了し、C# の catch には届かなかったことを示す。届かないC# の try の中で呼ぶAda で Constraint_ErrorGNAT のランタイムがメッセージを出すプロセスが終了するC# の catch

図10: Linux での実験では、Ada の例外は C# の catch に届かず、プロセスが終了した。

これは Linux で試した結果です。.NET の資料は、アンマネージドのコードの例外を扱えるのは Windows だけで、ほかのプラットフォームでは予測できない動作やクラッシュになりうるとしています10。Windows で同じことをしたときの動作は試していませんが、どちらのプラットフォームでも、Ada の例外を境界の外へ出さない設計にしておけば、この違いに左右されません。

公開する関数での例外の扱いエクスポートした関数の中で例外を処理すればエラーコードを返して呼び出し側が判断でき、処理しなければプロセスが終了することを示す。処理する処理しないC や C# から呼ぶAda の関数で例外が起きる関数の中で処理するかエラーコードを返すプロセスが終了する呼び出し側が判断する

図11: 公開する関数の中で例外を処理し、結果はエラーコードで返す。

Divide と Format は関数の中に例外処理を書いて、エラーコードや 0 を返しています。公開する関数はすべて、この形にしておきます。Add のような短い関数でも、オーバーフローで Constraint_Error になります。どの例外が起こりうるかを関数ごとに洗い出すか、when others で受けてエラーコードに変えます。

Ada から Windows の DLL を呼ぶ

Ada から Windows の API を呼ぶときも Import を使います。次の例は kernel32.dll の GetCurrentProcessId、GetTickCount64、GetComputerNameW を呼びます111213。

with Ada.Text_IO;  use Ada.Text_IO;
with Interfaces;   use Interfaces;
with Interfaces.C; use Interfaces.C;

procedure Use_Kernel32 is

   --  DWORD GetCurrentProcessId (void);
   function Get_Current_Process_Id return unsigned_long
     with Import, Convention => Stdcall, External_Name => "GetCurrentProcessId";

   --  ULONGLONG GetTickCount64 (void);
   function Get_Tick_Count_64 return Unsigned_64
     with Import, Convention => Stdcall, External_Name => "GetTickCount64";

   --  BOOL GetComputerNameW (LPWSTR lpBuffer, LPDWORD nSize);
   function Get_Computer_Name
     (Buffer : out wchar_array;
      Size   : in out unsigned_long) return int
     with Import, Convention => Stdcall, External_Name => "GetComputerNameW";

   Name   : wchar_array (0 .. 63);
   Length : unsigned_long := Name'Length;
begin
   Put_Line ("pid:" & unsigned_long'Image (Get_Current_Process_Id));
   Put_Line ("ticks > 0: " & Boolean'Image (Get_Tick_Count_64 > 0));

   if Get_Computer_Name (Name, Length) /= 0 then
      --  成功時の Length は終端の NUL を含まない文字数
      Put_Line ("computer name length:" & unsigned_long'Image (Length));
   else
      Put_Line ("GetComputerNameW failed");
   end if;
end Use_Kernel32;

Windows の API には Convention => Stdcall を指定しています。Win32 API の関数は __stdcall の呼び出し規約で呼びます14。GNAT の資料によると、64ビットの Windows には呼び出し規約が1つしかなく、規約の指定はすべて無視されます6。Windows 以外のプラットフォームでは、Stdcall は C として扱われます6。Stdcall と書いておけば、32ビットと64ビットのどちらでも同じ宣言で済みます。

GetComputerNameW の out wchar_array と in out unsigned_long は、参照マニュアルの対応どおり、配列の先頭へのポインターと DWORD へのポインターとして渡ります2。DWORD は unsigned long として宣言された32ビットの符号なし整数なので15、Ada では Interfaces.C.unsigned_long で受けています。

この例は Ubuntu の MinGW-w64 版 GNAT 13(x86_64-w64-mingw32-gnatmake)でビルドし、Wine 9.0 で実行しました。kernel32.dll のリンクを指定しなくてもビルドできました。

pid: 32
ticks > 0: TRUE
computer name length: 2

プロセス ID とコンピューター名の長さは Wine が返した値で、実際の Windows で動かしたときの値ではありません。32ビット版では試していません。

Windows で Convention => Stdcall が扱われる方法Convention => Stdcall の指定が、32ビットの Windows では stdcall として、64ビットの Windows では単一の呼び出し規約として、Windows 以外では C として扱われることを示す。32ビット Windows64ビット WindowsWindows 以外Convention => Stdcallプラットフォームstdcall で呼ぶ規約は1つなので無視C として扱う

図12: Stdcall の指定は32ビットの Windows でだけ呼び方を変える。

Windows の DLL として配る

同じ greet.gpr を MinGW-w64 版の GNAT でビルドすると、lib/libgreet.dll ができました。GNAT の資料も、Windows で DLL を作る手順に Windows に固有の部分はないとしています6。ただし、Ubuntu の gprbuild にクロスコンパイル用の設定をそのまま渡すとバインダーの選択が合わず、この環境ではバインダーを差し替えてビルドしました。この手順は Ubuntu の環境に固有のものなので、記事では扱いません。Windows の上でビルドするなら、Windows 用の GNAT を使います(Alire で GNAT を入れる方法は Alireで始めるAdaのプロジェクト を参照してください)。

この記事で Windows 向けを確かめた範囲Ubuntu の MinGW-w64 版 GNAT で libgreet.dll と C のプログラムを作り、Wine で実行して確かめ、実際の Windows と Windows 上の .NET では試していないことを示す。試していないMinGW-w64 版 GNAT(Ubuntu)libgreet.dllC のプログラム(main.exe)Wine 9.0 で実行実際の Windows と .NET

図13: Windows 向けの DLL は Ubuntu でビルドし、Wine で動かして確かめた。実際の Windows と .NET からの呼び出しは試していない。

x86_64-w64-mingw32-objdump -p で依存する DLL を見ると、次の4つでした。

	DLL Name: KERNEL32.dll
	DLL Name: msvcrt.dll
	DLL Name: libgcc_s_seh-1.dll
	DLL Name: libgnat-13.dll

libgnat-13.dll と libgcc_s_seh-1.dll は GNAT とともに入るランタイムの DLL です。この2つを libgreet.dll と同じフォルダーに置き、MinGW-w64 の gcc でビルドした C のプログラム(上の main.c)を Wine で動かすと、Linux と同じ出力になりました。libgnat-13.dll を取り除くと、Wine のローダーは libgnat-13.dll が見つからないというエラーを出し、状態コード c0000135 でプログラムが起動しませんでした。

Windows が DLL を探すフォルダーと順序は決まっています16。アプリのフォルダーに置くか、探す順序の中のどこに置くかを決めて、Ada のランタイムの DLL も一緒に配ります。探す順序の詳細は Windows DLL名前解決の仕組み にまとめています。

Ada の DLL を配るときに揃えるファイルC や C# のアプリが libgreet.dll を読み込み、libgreet.dll が libgnat-13.dll と libgcc_s_seh-1.dll に依存するので、3つを Windows が探すフォルダーに置くことを示す。C や C# のアプリlibgreet.dlllibgnat-13.dlllibgcc_s_seh-1.dllWindows が探すフォルダー

図14: Ada の DLL を配るときは、GNAT のランタイムの DLL も Windows が探すフォルダーに置く。

試した環境と確かめていないこと

この記事のコードは2026年11月1日に次の環境で動かしました。

  • Ubuntu 24.04、GNAT 13.3.0、Ubuntu の gprbuild パッケージ(2024.1)(共有ライブラリの作成、C からの呼び出し、Ada からの libc の呼び出し)
  • C# からの呼び出しは .NET SDK 10.0.114 で net8.0 向けにビルドし、.NET 8.0.33 のランタイムで実行
  • MinGW-w64 版 GNAT 13.2.0 と Wine 9.0(Windows 向けの DLL と実行ファイル)

実際の Windows での実行、32ビット版の Windows、Windows 上の .NET からの呼び出し、Windows で Ada の例外が境界を越えたときの動作は確かめていません。これらの点は本文で示した GNAT と Microsoft の資料に基づいています。

参考リンク

  1. Ada Reference Manual, B.1 Interfacing Aspects. Import、Export、Convention、External_Name の定義。 ↩

  2. Ada Reference Manual, B.3 Interfacing with C and C++. Interfaces.C の型と、C との引数の渡し方の対応(Implementation Advice)。 ↩ ↩2 ↩3

  3. Ada 2012 Reference Manual, B.3.1 The Package Interfaces.C.Strings. New_String、Free、Value、Update の定義と注意。 ↩ ↩2 ↩3 ↩4

  4. AdaCore, GPRbuild and GPR Companion Tools User’s Guide: GNAT Project Manager. Library_Kind、Library_Interface、Library_Auto_Init、Library_Standalone の説明。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  5. Ada Reference Manual, 10.2 Program Execution. パーティションの実行とライブラリユニットの初期化(elaboration)。 ↩

  6. AdaCore, GNAT User’s Guide: Platform-Specific Information. Windows の呼び出し規約、DLL の作成と初期化、Ada の DLL の制限。 ↩ ↩2 ↩3 ↩4 ↩5

  7. Microsoft Learn, P/Invoke source generation. LibraryImport と DllImport の違い、AllowUnsafeBlocks の要件。 ↩ ↩2 ↩3

  8. Microsoft Learn, Calling conventions. x86 の Windows での既定は Stdcall で、Cdecl のライブラリには明示が要ること。x64・ARM・ARM64 では呼び出し規約が1つであること。 ↩

  9. Microsoft Learn, Native library loading. プラットフォームごとに試すライブラリ名の変化。 ↩

  10. Microsoft Learn, Exceptions Interoperability. アンマネージドのコードの例外の扱いがプラットフォームによって違うこと。 ↩

  11. Microsoft Learn, GetCurrentProcessId function. ↩

  12. Microsoft Learn, GetTickCount64 function. ↩

  13. Microsoft Learn, GetComputerNameW function. ↩

  14. Microsoft Learn, __stdcall. Win32 API の関数の呼び出しに使う規約。x64 と ARM ではコンパイラが指定を受け付けて無視する。 ↩

  15. Microsoft Learn, Windows Data Types. DWORD と WINAPI の定義。 ↩

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Adaで作ったDLLをC#から呼ぶとき、何を決めておけばよいですか?
公開する関数をConvention => Cでエクスポートし、引数をInterfaces.Cの型で書きます。そのうえで、ライブラリの初期化を誰がいつ行うか、文字列の領域をどちらが確保して解放するか、Adaの例外を境界の外へ出さないことを決めておきます。
Adaの例外はC#のtry/catchで捕まえられますか?
この記事のLinuxでの実験では捕まえられませんでした。処理されないConstraint_ErrorでGNATのランタイムがメッセージを出し、プロセスごと終了しました。エクスポートする関数の中で例外を処理し、結果は戻り値のエラーコードで返します。
gprbuildで作ったWindows用のDLLをC#のDllImportで読み込めないのはなぜですか?
名前の違いが原因の一つです。gprbuildはライブラリ名greetからlibgreet.dllを作りますが、.NETはWindowsではlibを前に付けて探しません。DllImportやLibraryImportには"libgreet"と書きます。DLLが依存するlibgnat-13.dllなども、読み込めるフォルダーに置く必要があります。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る