Revision history (first version, published Nov 1, 2026)
- First published
Cite this article(DOI: 10.5281/zenodo.23856518)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Connecting Ada with C and C# — Building a DLL and Calling It. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23856518 https://comcomponent.com/en/blog/ada-c-csharp-dll-interop/
- DOI (latest version)
- 10.5281/zenodo.23856518
- DOI (this version)
- 10.5281/zenodo.23856519
Sometimes you want to use logic written in Ada from a C or C# application. In the other direction, you may want to call Windows APIs or an existing DLL from an Ada program. Ada defines how to interface with other languages in Annex B of its Reference Manual, so adding Convention => C to a function declaration lets you export or call a function that uses the same calling style as C.
Exporting the functions is not enough, though. An Ada library has initialization (elaboration) that must be done before it is used, and you also have to decide which side allocates and frees string buffers. If an Ada exception leaves the boundary, the caller’s process may end. This article builds a small library as a shared library with gprbuild and calls it from C and C#, explaining each of these points in turn. At the end, it covers an example of calling a Windows DLL from Ada and what to check when distributing the library as a Windows DLL.
Every example was actually built and run. On Linux (Ubuntu 24.04) I built a shared library (.so) with GNAT 13.3.0 and gprbuild, called it from C with gcc and from C# with .NET 8. For Windows, I built the DLL and executables with the MinGW-w64 build of GNAT 13 on Ubuntu and ran them on Wine 9.0. Because nothing was run on real Windows, the Windows-specific points cite GNAT and Microsoft documentation, and each section states what was measured and what was not. For an introduction to Ada itself, see The Appeal of the Ada Language.
What to decide at the Ada–C boundary
Annex B of the Ada Reference Manual defines Import, which calls subprograms in other languages, and Export, which makes Ada subprograms callable from other languages1. You choose which language’s calling style to follow with Convention; to match C, write Convention => C. The name used at link time is set with External_Name.
flowchart TB
accTitle: The boundary between Ada, C and C#
accDescr: Shows that the Ada library exports functions in the C calling style with Convention => C, that C calls the same functions directly and C# through P/Invoke, and that Ada calls Windows DLLs with Import.
A["Ada library"] -->|"Export and Convention => C"| B["Functions in the C calling style"]
C["C program"] --> B
D["C# program"] -->|"P/Invoke"| B
A -->|"Import"| E["Windows DLL"]
Figure 1: Ada exports functions in the C calling style, and C and C# call them. To call a Windows DLL from Ada, use Import.
For parameter and return types, use the types in the package Interfaces.C. int, size_t, char_array and the others are defined to correspond to the C types of the same name2. Ada’s Integer and String are not defined to have the same representation as the C types, so do not expose them directly at the boundary. An Ada String carries its index range and has no terminating NUL like a C string. Receive a C char * with chars_ptr from Interfaces.C.Strings3.
There are also rules for how parameters are passed. The Reference Manual advises mapping in scalar parameters to C pass-by-value, out and in out parameters of elementary types (scalar types and the like) to pointers (t*), and array parameters to pointers to their elements2. In this article’s example, the C int *result is received as Result : out int in Ada.
flowchart TB
accTitle: C and Ada type mappings used in this article's examples
accDescr: Shows that C int, size_t, char * and int * are received in Ada as Interfaces.C int, size_t, chars_ptr and an out int parameter, respectively.
A["C int"] --> B["Ada int"]
C["C size_t"] --> D["Ada size_t"]
E["C char *"] --> F["Ada chars_ptr"]
G["C int *"] --> H["Ada out int parameter"]
Figure 2: In this article’s examples, C types are received with the same-named types in Interfaces.C and chars_ptr, and int * is received as an out parameter.
Besides types, three more things have to be decided at the boundary: who initializes the library and when, which side allocates and frees string and buffer memory, and how to handle Ada exceptions. The examples below show how to decide each of them.
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (15 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
Writing Ada functions in a form C can call
As an example, we build a package Greet_API with four functions: addition, division, building a greeting, and counting calls. In the spec (.ads), add Export, Convention => C and External_Name to the functions to be exported.
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);
-- Return value: 0 = success, 1 = division by 0, 2 = result does not fit in 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);
-- Returns the length (including the terminating NUL) needed for "Hello, <name>!".
-- Writes to buffer only when buffer is not NULL and length is large enough.
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); Returns the value incremented by 1 on each call
function Counter return int
with Export, Convention => C, External_Name => "greet_counter";
end Greet_API;
The body (.adb) is as follows. Divide handles exceptions inside the function and returns an error code, and Format writes the string into the memory passed in by the caller. The variable that Counter counts with is set to 100 after the begin at the end of the package body, that is, in the initialization code. This value is used later to tell whether initialization has run.
with System; use type System.Address;
package body Greet_API is
-- Set to 100 during package initialization (elaboration)
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
-- Treat the caller's memory as a char_array of the same length as Text
Target : char_array (Text'Range)
with Import, Address => Buffer;
begin
Target := Text;
end;
end if;
return Text'Length;
end;
exception
when others =>
return 0; -- Do not let the exception reach the C side
end Format;
function Counter return int is
begin
Calls := Calls + 1;
return Calls;
end Counter;
begin
Calls := 100;
end Greet_API;
flowchart TB
accTitle: Processing flow of greet_format
accDescr: Shows that greet_format returns 0 if name is NULL; otherwise it builds the string, writes it only when buffer is not NULL and long enough, and then returns the required length.
A{"Is name NULL?"} -->|"NULL"| B["Return 0"]
A -->|"Not NULL"| C["Build Hello, name!"]
C --> D{"Buffer given and long enough?"}
D -->|"Long enough"| E["Write to buffer"]
D -->|"Too short"| F["Do not write"]
E --> G["Return the required length"]
F --> G
Figure 3: greet_format writes only when the buffer is large enough, and always returns the required length.
Format writes by overlaying an Ada array on the caller’s memory through Address. Interfaces.C.Strings also has Update, which rewrites what a chars_ptr points to. According to the Reference Manual, when Check => True, Update raises Update_Error if the write goes past the length of the existing string. With Check => False, a write that would raise Update_Error under True is erroneous execution3. Memory the caller has just allocated does not yet contain a string, so Update is not used here.
Making a shared library with gprbuild
The shared library is built from a gprbuild project file. Setting Library_Kind to "dynamic" produces an independent library that is loaded at run time4. Listing the units to expose in Library_Interface makes it a stand-alone library. A stand-alone library is the form suited to using the Ada part from programs written in languages other than Ada4.
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;
Building with gprbuild -p -P greet.gpr produces 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
Listing the exported names with nm -D shows greetinit and greetfinal in addition to the four names given with External_Name. These are the initialization and finalization procedures that the binder creates from the library name4.
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
flowchart TB
accTitle: How gprbuild builds a stand-alone library
accDescr: Shows that the Ada sources are compiled, the binder creates a file containing the initialization procedure, and they are linked into a shared library.
A["greet_api.ads and .adb"] -->|"Compile"| B["Object files"]
B -->|"Bind"| C["Initialization procedure (greetinit)"]
B --> D["Link"]
C --> D
D --> E["libgreet.so (libgreet.dll on Windows)"]
Figure 4: gprbuild compiles, binds and links, producing a shared library that includes the initialization procedure.
This library depends on the GNAT runtime. The output of ldd lib/libgreet.so included libgnat-13.so. When distributing to another machine, provide this runtime as well. The GPRbuild documentation also describes Library_Standalone use "encapsulated", which pulls in the runtime to remove the dependency4. However, when I tried it in this environment (Ubuntu’s GNAT 13), libgnat-13.so was still in the ldd output even with encapsulated. Check the dependencies with your own toolchain before distributing.
Who performs initialization (elaboration)
Ada packages need initialization (elaboration) before they are used5. This covers the statements after begin in a package body and the computation of variables’ initial values. For an Ada main program, the code generated by the binder does this at startup. For a library called from C or C#, the library side has to do it.
If Library_Auto_Init is not specified or is set to "true", initialization runs when the library is loaded on platforms that support automatic initialization4. If it is set to "false", it does not run automatically, and the initialization procedure (here, greetinit) must be called before using the library’s functions4.
To check the difference, I ran a C program that calls only Counter, linked against a library built with Library_Auto_Init set to "true" and one set to "false". With "true" the result was counter: 101; with "false" it was counter: 1. In the "false" case, Calls := 100; in the package body has not run.
counter: 1 (Library_Auto_Init = "false", greetinit not called)
counter: 101 (Library_Auto_Init = "true")
When greet_format was called without initialization, the process ended with a segmentation fault in this environment (I did not investigate the cause). Forgetting initialization will not always fail this obviously, so if you do not use automatic initialization, always call the initialization procedure from the caller.
#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(); /* Explicitly run the Ada-side initialization (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;
}
When greetinit was called first, the "false" library also worked correctly.
format: Hello, Ada!
counter: 101
flowchart TB
accTitle: The Library_Auto_Init setting and the initialization flow
accDescr: Shows that if Library_Auto_Init is true, initialization runs when the library is loaded; if false, the caller calls greetinit before using the functions, and without that call the package body initialization does not run.
A{"Library_Auto_Init"} -->|"true"| B["Initialized at load time"]
A -->|"false"| C{"Was greetinit called?"}
C -->|"Called"| D["Initialized"]
C -->|"Not called"| E["Body initialization does not run"]
B --> F["Call the functions"]
D --> F
Figure 5: Without automatic initialization, the caller calls the initialization procedure before using the functions.
On Windows, automatic initialization has a restriction. According to the GNAT documentation, threads cannot be created inside DllMain on Windows, so tasks declared at library level cannot be used in a stand-alone library6. The same documentation also says that if there are library-level tasks, initialization cannot be called from DllMain6. Ada tasks are covered in Safe Concurrency with Ada.
Calling from C
On the C side, write the C declarations from the Ada spec as the function declarations, and link the library. Because this uses the library built with Library_Auto_Init set to "true", greetinit is not called.
#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));
/* Ask for the required length on the first call, allocate on the caller side, then have the second call write */
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
At build time, the library is linked with -L and -lgreet; at run time, LD_LIBRARY_PATH points to the folder containing libgreet.so.
flowchart TB
accTitle: Using the Ada library from a C program
accDescr: Shows that the program is linked with -lgreet by gcc, libgreet.so is loaded at run time from the LD_LIBRARY_PATH folder, initialization runs at load time, and then the functions are called.
A["Link with -lgreet using gcc"] --> B["Run"]
B --> C["Load libgreet.so from LD_LIBRARY_PATH"]
C --> D["Initialized at load time"]
D --> E["Call greet_add and others"]
Figure 6: The C program loads the linked libgreet.so at run time and calls the functions after initialization.
Here is the output. Dividing by 0, and a result that does not fit in int such as INT_MIN / -1, each raised Constraint_Error inside Ada, and the exception handler in Divide returned 1 and 2 respectively. Because counter starts at 101, initialization ran at load time.
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
Calling from C#
From C#, call through P/Invoke. Since .NET 7, the LibraryImport attribute is available and generates the marshalling code at compile time7. The traditional DllImport generates an IL stub at run time7. The following example declares three functions with LibraryImport and one with DllImport. The full picture of writing P/Invoke is in Safely Calling Win32 APIs from C#.
flowchart TB
accTitle: How LibraryImport and DllImport produce marshalling code
accDescr: Shows that LibraryImport generates the marshalling code at compile time, DllImport generates an IL stub at run time, and both call the Ada library's functions.
A["LibraryImport"] -->|"Compile time"| B["Generate marshalling code"]
C["DllImport"] -->|"Run time"| D["Generate IL stub"]
B --> E["Ada library function"]
D --> E
Figure 7: LibraryImport creates the calling code at compile time, and DllImport at run time.
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 _)}");
// Ask for the required length on the first call, then have the second call write into an array allocated on the C# side
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" is searched for as libgreet.so on Linux and greet.dll on Windows
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);
// The traditional DllImport can call the same function
[DllImport(Lib, EntryPoint = "greet_counter",
CallingConvention = CallingConvention.Cdecl)]
internal static extern int CounterOld();
}
Because the Ada side exports with Convention => C, the C# side also sets the calling convention to Cdecl. On x86 Windows the P/Invoke default is Stdcall, so without this setting the calling conventions would not match. On x64, ARM and ARM64 there is only one calling convention, and this setting does not change the result8. The C size_t is received as nuint in C#. Because the string parameter specifies StringMarshalling.Utf8, it is passed to Ada as a NUL-terminated UTF-8 string. Projects that use LibraryImport need AllowUnsafeBlocks enabled7.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
Building without AllowUnsafeBlocks produced the error SYSLIB1062: LibraryImportAttribute requires unsafe code. Here is the result of building with it enabled and running with the library folder given in 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
The "greet" written in DllImport and LibraryImport is not the file name itself. .NET searches while varying the name to suit the platform. On Linux and macOS it also tries names with a lib prefix in addition to the extension, but on Windows it tries only greet and greet.dll9. On the other hand, building for Windows with gprbuild produces libgreet.dll, as shown later. To use it on Windows, either write "libgreet" or rename the DLL.
flowchart TB
accTitle: File names .NET searches for from a library name
accDescr: Shows that when DllImport is given greet, Linux tries greet.so, libgreet.so and others, while Windows tries only greet and greet.dll, so the libgreet.dll built by gprbuild is not found.
A["DllImport (greet)"] --> B{"OS it runs on"}
B -->|"Linux"| C["Try greet.so, libgreet.so, etc."]
B -->|"Windows"| D["Try greet and greet.dll"]
C --> E["libgreet.so is found"]
D --> F["libgreet.dll is not found"]
F -.-> G["Write libgreet"]
Figure 8: On Windows, .NET does not add a lib prefix when searching, so write libgreet for the libgreet.dll that gprbuild builds.
Which side allocates and which side frees strings and memory
For strings and buffers that cross the boundary, decide which side allocates the memory and which side frees it. Memory must be freed by the same mechanism that allocated it.
In this article, greet_format has the caller allocate the memory. On the first call, the caller passes NULL as buffer and receives the required length; it then allocates memory of that length and has the second call write into it. C allocates with malloc and free, C# with byte[], and the Ada side only writes. Because allocation and freeing are completed on the same side, you do not have to worry about differences between runtimes.
flowchart TB
accTitle: Passing a string in memory allocated by the caller
accDescr: Shows that the caller passes NULL as buffer to ask for the required length, allocates that length and calls again, Ada writes, and finally the caller frees the memory itself.
A["Caller calls with NULL"] --> B["Ada returns the required length"]
B --> C["Caller allocates"]
C --> D["Caller calls again"]
D --> E["Ada writes"]
E --> F["Caller frees"]
Figure 9: The caller is responsible for allocating and freeing, and Ada only writes into the memory it is given.
If the Ada side allocates and returns a string, the library should also export a function to free it. The Reference Manual says that memory allocated by New_String in Interfaces.C.Strings is to be freed with Free, not with a C function3. An implementation of New_String may use either the C allocation function or Ada’s allocation, so freeing it with the caller’s free is not guaranteed to be safe. Conversely, passing to Free a chars_ptr obtained from anything other than New_String or New_Char_Array is also erroneous execution3.
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); -- Return memory allocated by New_String with Free
end Use_Libc;
This example calls strlen and abs from the C standard library from Ada with Import. It passes a string created with New_String to the C function and frees it with Ada’s Free when done. Building with gnatmake use_libc.adb on Linux and running it printed the following.
strlen: 12
abs: 42
Ada memory management as a whole is covered in Memory Management in Ada.
Keep exceptions from leaving the boundary
I tested what happens when an exception not handled inside an Ada function occurs during a call from C. I ran a C program that passes INT_MAX and 1 to the greet_add above. Because Add does not handle exceptions, a Constraint_Error occurs inside Ada.
#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
The GNAT runtime printed a message, and the process exited with exit code 1. after was not printed. Calling the same function from C# wrapped in try and catch (Exception) gave the same result: control never entered catch, and the process exited with the same message.
flowchart TB
accTitle: What happens when an Ada exception occurs inside a C# try
accDescr: Shows that when greet_add is called from inside a C# try and a Constraint_Error occurs in Ada, the GNAT runtime prints a message and the process exits, and the exception never reached the C# catch.
A["Call from inside a C# try"] --> B["Constraint_Error in Ada"]
B --> C["GNAT runtime prints a message"]
C --> D["Process exits"]
B -.->|"Never reaches"| E["C# catch"]
Figure 10: In the experiment on Linux, the Ada exception never reached the C# catch, and the process exited.
This is the result on Linux. The .NET documentation states that exceptions from unmanaged code can be handled only on Windows, and that on other platforms they can lead to unpredictable behavior or crashes10. I have not tried the same thing on Windows, but on either platform, a design that keeps Ada exceptions from leaving the boundary is not affected by this difference.
flowchart TB
accTitle: Handling exceptions in exported functions
accDescr: Shows that if an exported function handles the exception internally, it returns an error code and the caller can decide what to do, and if it does not, the process exits.
A["Called from C or C#"] --> B["Exception in the Ada function"]
B --> C{"Handled inside the function?"}
C -->|"Handled"| D["Return an error code"]
C -->|"Not handled"| E["Process exits"]
D --> F["Caller decides"]
Figure 11: Handle exceptions inside the exported functions and return the result as an error code.
Divide and Format contain exception handlers and return an error code or 0. Give every exported function this form. Even a short function like Add raises Constraint_Error on overflow. Either enumerate which exceptions each function can raise, or catch them with when others and turn them into error codes.
Calling a Windows DLL from Ada
Calling Windows APIs from Ada also uses Import. The following example calls GetCurrentProcessId, GetTickCount64 and GetComputerNameW in kernel32.dll111213.
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
-- On success, Length is the number of characters, not counting the terminating NUL
Put_Line ("computer name length:" & unsigned_long'Image (Length));
else
Put_Line ("GetComputerNameW failed");
end if;
end Use_Kernel32;
The Windows API functions are given Convention => Stdcall. Win32 API functions are called with the __stdcall calling convention14. According to the GNAT documentation, 64-bit Windows has only one calling convention, and all convention specifications are ignored6. On platforms other than Windows, Stdcall is treated as C6. Writing Stdcall lets the same declaration work on both 32-bit and 64-bit.
The out wchar_array and in out unsigned_long of GetComputerNameW are passed, as the Reference Manual’s mapping says, as a pointer to the first element of the array and a pointer to a DWORD2. DWORD is a 32-bit unsigned integer declared as unsigned long15, so Ada receives it with Interfaces.C.unsigned_long.
This example was built with the MinGW-w64 build of GNAT 13 on Ubuntu (x86_64-w64-mingw32-gnatmake) and run on Wine 9.0. It built without specifying kernel32.dll for linking.
pid: 32
ticks > 0: TRUE
computer name length: 2
The process ID and the length of the computer name are values returned by Wine, not values from running on real Windows. The 32-bit version was not tested.
flowchart TB
accTitle: How Windows treats Convention => Stdcall
accDescr: Shows that Convention => Stdcall is treated as stdcall on 32-bit Windows, as the single calling convention on 64-bit Windows, and as C on platforms other than Windows.
A["Convention => Stdcall"] --> B{"Platform"}
B -->|"32-bit Windows"| C["Call with stdcall"]
B -->|"64-bit Windows"| D["Ignored: only one convention"]
B -->|"Not Windows"| E["Treated as C"]
Figure 12: The Stdcall specification changes the calling style only on 32-bit Windows.
Distributing as a Windows DLL
Building the same greet.gpr with the MinGW-w64 build of GNAT produced lib/libgreet.dll. The GNAT documentation also says that the procedure for building a DLL on Windows has nothing Windows-specific6. However, passing a cross-compilation configuration directly to Ubuntu’s gprbuild selected the wrong binder, so in this environment I built it with the binder swapped. That step is specific to the Ubuntu environment, so this article does not cover it. If you build on Windows, use GNAT for Windows (for installing GNAT with Alire, see Starting an Ada Project with Alire).
flowchart TB
accTitle: What this article verified for Windows
accDescr: Shows that libgreet.dll and a C program were built with the MinGW-w64 build of GNAT on Ubuntu and checked by running them on Wine, and that real Windows and .NET on Windows were not tested.
A["MinGW-w64 GNAT (Ubuntu)"] --> B["libgreet.dll"]
A --> C["C program (main.exe)"]
B --> D["Run on Wine 9.0"]
C --> D
E["Real Windows and .NET"] -.->|"Not tested"| B
Figure 13: The Windows DLL was built on Ubuntu and checked by running it on Wine. Calls from real Windows and .NET were not tested.
Listing the DLLs it depends on with x86_64-w64-mingw32-objdump -p showed these four.
DLL Name: KERNEL32.dll
DLL Name: msvcrt.dll
DLL Name: libgcc_s_seh-1.dll
DLL Name: libgnat-13.dll
libgnat-13.dll and libgcc_s_seh-1.dll are runtime DLLs installed with GNAT. With these two in the same folder as libgreet.dll, a C program built with the MinGW-w64 gcc (the main.c above) produced the same output on Wine as on Linux. When libgnat-13.dll was removed, the Wine loader reported that libgnat-13.dll could not be found, and the program did not start, with status code c0000135.
The folders Windows searches for DLLs, and their order, are fixed16. Decide whether to put the DLLs in the application folder or somewhere else in the search order, and distribute the Ada runtime DLLs with them. The details of the search order are in How Windows DLL Name Resolution Works.
flowchart TB
accTitle: Files to ship together when distributing an Ada DLL
accDescr: Shows that a C or C# application loads libgreet.dll, which depends on libgnat-13.dll and libgcc_s_seh-1.dll, so all three go in a folder Windows searches.
A["C or C# application"] --> B["libgreet.dll"]
B --> C["libgnat-13.dll"]
B --> D["libgcc_s_seh-1.dll"]
E["Folder Windows searches"] -.-> B
E -.-> C
E -.-> D
Figure 14: When distributing an Ada DLL, also put the GNAT runtime DLLs in a folder Windows searches.
Test environment and what was not verified
The code in this article was run on November 1, 2026, in the following environments.
- Ubuntu 24.04, GNAT 13.3.0, Ubuntu’s gprbuild package (2024.1) (building the shared library, calling from C, calling libc from Ada)
- Calls from C# were built for
net8.0with .NET SDK 10.0.114 and run on the .NET 8.0.33 runtime - MinGW-w64 build of GNAT 13.2.0 and Wine 9.0 (the Windows DLL and executables)
Running on real Windows, 32-bit Windows, calling from .NET on Windows, and the behavior when an Ada exception crosses the boundary on Windows were not verified. Those points are based on the GNAT and Microsoft documentation cited in the text.
References
-
Ada Reference Manual, B.1 Interfacing Aspects. Definitions of Import, Export, Convention and External_Name. ↩
-
Ada Reference Manual, B.3 Interfacing with C and C++. The types in
Interfaces.Cand the mapping of parameter passing to C (Implementation Advice). ↩ ↩2 ↩3 -
Ada 2012 Reference Manual, B.3.1 The Package Interfaces.C.Strings. Definitions of and cautions for
New_String,Free,ValueandUpdate. ↩ ↩2 ↩3 ↩4 -
AdaCore, GPRbuild and GPR Companion Tools User’s Guide: GNAT Project Manager. Descriptions of Library_Kind, Library_Interface, Library_Auto_Init and Library_Standalone. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Ada Reference Manual, 10.2 Program Execution. Execution of a partition and initialization (elaboration) of library units. ↩
-
AdaCore, GNAT User’s Guide: Platform-Specific Information. Calling conventions on Windows, building and initializing DLLs, and restrictions on Ada DLLs. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, P/Invoke source generation. Differences between LibraryImport and DllImport, and the AllowUnsafeBlocks requirement. ↩ ↩2 ↩3
-
Microsoft Learn, Calling conventions. The default on x86 Windows is Stdcall, and Cdecl libraries must specify it explicitly. x64, ARM and ARM64 have one calling convention. ↩
-
Microsoft Learn, Native library loading. How the library names tried vary by platform. ↩
-
Microsoft Learn, Exceptions Interoperability. How the handling of exceptions from unmanaged code differs by platform. ↩
-
Microsoft Learn, GetCurrentProcessId function. ↩
-
Microsoft Learn, GetTickCount64 function. ↩
-
Microsoft Learn, GetComputerNameW function. ↩
-
Microsoft Learn, __stdcall. The convention used to call Win32 API functions. On x64 and ARM, the compiler accepts and ignores it. ↩
-
Microsoft Learn, Windows Data Types. Definitions of
DWORDandWINAPI. ↩ -
Microsoft Learn, Dynamic-link library search order. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
DLL and COM Interface Backward Compatibility — A Decision Table for Which Changes Break Callers
Which changes to a DLL or COM component actually break their callers? We lay out the three layers of compatibility — binary, source, and ...
Do Business Apps Run on Windows on Arm? — The Reality of x64 Emulation (Prism) and Native DLLs/COM
An answer, aimed at developers and IT staff, to 'will our business app run on Windows on Arm?' Covers how x64 emulation (Prism) works, th...
Safely Calling Win32 APIs from C# — A Practical P/Invoke Guide (DllImport / LibraryImport / CsWin32)
A practical rundown of what to watch for when calling Win32 APIs and native DLLs from C# via P/Invoke. Covers the differences between Dll...
Memory Management in Ada — When to Use Access Types, Controlled Types, and Storage Pools
Ada does not rely on GC: values go on the stack first, and when the heap is needed, access types put lifetime rules into types. Deallocat...
Starting an Ada Project with Alire — Creating a Project, Adding Dependencies, Build Profiles, and GNATprove
Using Alire, the Ada package manager, to create a project, fetch GNAT automatically, add crates, switch build profiles, and prove code wi...
Related Topics
These are topic pages close to this theme. Starting from the article, you can go on to related services and other articles.
Windows Technical Topics
Entry point collecting technical topics on Windows development, bug investigation, and use of existing assets.
Services Connected to This Topic
This article leads to the following service pages. Start from the entry point closest to your situation.
Windows App Development
If you want to package calculation or control logic written in Ada as a DLL that existing C or C# applications can call, we can take this on as part of Windows application development.
Technical Consulting & Design Review
If you want to sort out how to decide types, string ownership, initialization and exceptions at a cross-language boundary, we can handle it as technical consulting and design review.
Frequently Asked Questions
Questions commonly asked at the time of consultation about the topic of this article.
- What should I decide before calling an Ada DLL from C#?
- Export the public functions with Convention => C and write their parameters with the types from Interfaces.C. Then decide who initializes the library and when, which side allocates and frees string buffers, and that Ada exceptions must not leave the boundary.
- Can C#'s try/catch catch an Ada exception?
- In this article's experiment on Linux, it could not. An unhandled Constraint_Error made the GNAT runtime print a message, and the whole process exited. Handle exceptions inside the exported functions and return the result as an error code.
- Why can't C#'s DllImport load a Windows DLL built with gprbuild?
- A difference in names is one cause. gprbuild builds libgreet.dll from the library name greet, but on Windows .NET does not search with a lib prefix. Write "libgreet" in DllImport or LibraryImport. The DLLs it depends on, such as libgnat-13.dll, also need to be in a folder from which they can be loaded.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and bug investigation, with particular strength in projects where existing assets remain and in investigating failures whose cause is hard to see.