Ada로 C 동적 라이브러리 우아하게 다루기
많은 시스템 프로그래밍에서 동적 라이브러리(Windows의 .dll
, 리눅스의 .so
)를 런타임에 로드하여 사용하는 것은 매우 흔한 일입니다. C언어에서는 <dlfcn.h>
헤더의 dlopen
, dlsym
, dlclose
함수를 통해 이 기능을 사용합니다.
Ada는 강력한 타입 시스템과 안전성을 자랑하지만, 이러한 저수준 C 인터페이스를 직접 다루는 것은 번거롭고 오류가 발생하기 쉽습니다.
이 글에서는 Ada의 강력한 기능을 활용하여 C의 dlopen
인터페이스를 안전하고 우아하게 감싸는 ‘래퍼(Wrapper)’를 만드는 전체 과정을, 컴파일 가능한 전체 소스 코드와 함께 안내합니다.
프로젝트 파일 구조
먼저, 오늘 만들 프로젝트는 다음과 같은 파일들로 구성됩니다.
.
├── dl.ads # Ada 래퍼 패키지 명세
├── dl.adb # Ada 래퍼 패키지 구현
├── dl-open.c # C 헬퍼 함수
├── libexample.c # 동적으로 로드할 C 공유 라이브러리
├── main.adb # 래퍼를 사용하는 Ada 메인 프로그램
└── Makefile # 프로젝트 빌드 자동화 스크립트
전체 소스 코드
아래 소스 코드를 각 파일 이름에 맞게 저장해주세요.
1. 예제 C 공유 라이브러리 (libexample.c
)
Ada에서 동적으로 로드할 대상입니다. 간단한 함수와 전역 변수를 가집니다.
// libexample.c
#include <stdio.h>
int my_global_variable = 123;
int my_function (int x)
{
printf (" (C) my_function called with %d\n", x);
printf (" (C) Current my_global_variable = %d\n", my_global_variable);
my_global_variable += x;
printf (" (C) New my_global_variable = %d\n", my_global_variable);
return x * x;
}
2. C 헬퍼 함수 (dl-open.c
)
dlopen
의 mode
인자는 C 헤더의 매크로 상수를 비트 연산으로 조합합니다. 이 과정을 C에서 처리하게 하여 Ada 코드의 복잡성을 줄여주는 작은 도우미 함수입니다.
// dl-open.c
#include <dlfcn.h>
void* dl_open (const char* path, int mode)
{
int new_mode = 0;
if (mode & 1) new_mode |= RTLD_LAZY;
if (mode & 2) new_mode |= RTLD_NOW;
if (mode & 4) new_mode |= RTLD_LOCAL;
if (mode & 8) new_mode |= RTLD_GLOBAL;
return dlopen (path, new_mode);
}
3. Ada 래퍼 패키지 명세 (dl.ads
)
사용자에게 보여줄 깔끔한 인터페이스입니다. 에러 상황을 위한 예외(exception)들과 dlopen
모드 상수를 정의합니다.
-- dl.ads
with System;
package Dl is
Library_Load_Error : Exception;
Library_Close_Error : Exception;
Symbol_Lookup_Error : Exception;
RTLD_LAZY : constant Integer := 1;
RTLD_NOW : constant Integer := 2;
RTLD_LOCAL : constant Integer := 4;
RTLD_GLOBAL : constant Integer := 8;
function open (path : in String;
mode : in Integer)
return System.Address;
procedure close (handle : in System.Address);
function get_symbol (handle : in System.Address;
sym_name : in String)
return System.Address;
end Dl;
4. Ada 래퍼 패키지 구현 (dl.adb
)
C 함수를 호출하고, 반환값을 확인하여 에러 발생 시 Ada 예외를 던지는 핵심 로직이 담겨 있습니다.
-- dl.adb
with Interfaces.C;
with Interfaces.C.Strings;
with Ada.Unchecked_Conversion;
package body Dl is
use Interfaces.C;
use Interfaces.C.Strings;
use System;
function dlopen (path : Interfaces.C.Strings.Chars_Ptr;
flag : Interfaces.C.Int) return System.Address;
pragma import (c, dlopen, "dl_open");
function dlclose (handle : System.Address) return Interfaces.C.Int;
pragma import (c, dlclose, "dlclose");
function dlerror return Interfaces.C.Strings.Chars_Ptr;
pragma import (c, dlerror, "dlerror");
function dlsym (handle : System.Address;
symbol : Interfaces.C.Strings.Chars_Ptr)
return System.Address;
pragma import (c, dlsym, "dlsym");
function to_chars_ptr is new Ada.Unchecked_Conversion (
Source => System.Address,
Target => Interfaces.C.Strings.Chars_Ptr
);
function open (path : String;
mode : Integer) return System.Address is
handle : System.Address;
c_path : constant Interfaces.C.Char_Array :=
Interfaces.C.to_c (path) & Interfaces.C.Nul;
begin
handle := dlopen (to_chars_ptr (c_path'address),
Interfaces.C.Int (mode));
if handle = System.Null_Address then
declare
errmsg_ptr : constant Interfaces.C.Strings.Chars_Ptr := dlerror;
errmsg : constant String :=
(if errmsg_ptr /= Interfaces.C.Strings.Null_Ptr then
Interfaces.C.Strings.value (errmsg_ptr)
else
"Unknown dlopen error (dlerror returned NULL)");
begin
raise Library_Load_Error with "dlopen(" & path & "," & mode'image &
") failed: " & errmsg;
end;
end if;
return handle;
end open;
procedure close (handle : in System.Address) is
retval : Interfaces.C.Int;
begin
retval := dlclose (handle);
if retval /= 0 then
declare
errmsg_ptr : constant Interfaces.C.Strings.chars_ptr := dlerror;
errmsg : constant String := (
if errmsg_ptr /= Interfaces.C.Strings.Null_Ptr then
Interfaces.C.Strings.value (errmsg_ptr)
else "Unknown dlclose() error; dlerror() returns NULL");
begin
raise Library_Close_Error with "dlclose failed: " & errmsg;
end;
end if;
end close;
function get_symbol (handle : in System.Address;
sym_name : in String) return System.Address is
sym_addr : System.Address;
c_sym_name : constant Interfaces.C.Char_Array :=
Interfaces.C.to_c (sym_name) & Interfaces.C.Nul;
errmsg_ptr : Interfaces.C.Strings.Chars_Ptr;
dummy_ptr : Interfaces.C.Strings.Chars_Ptr;
begin
if handle = System.Null_Address then
raise Program_Error
with "Attempt to call dlsym with a null library handle.";
end if;
dummy_ptr := dlerror;
sym_addr := dlsym (handle, to_chars_ptr (c_sym_name'address));
if sym_addr = System.Null_Address then
errmsg_ptr := dlerror;
if errmsg_ptr /= Interfaces.C.Strings.Null_Ptr then
declare
errmsg : constant String := Interfaces.C.Strings.Value (errmsg_ptr);
begin
raise Symbol_Lookup_Error
with "dlsym lookup for '" & sym_name & "' failed: " & errmsg;
end;
end if;
end if;
return sym_addr;
end get_symbol;
end Dl;
5. 메인 실행 프로그램 (main.adb
)
Dl
래퍼를 사용하여 libexample.so
를 로드하고, 내부의 함수와 전역 변수를 직접 사용하는 예제입니다.
-- main.adb
with Ada.Text_IO;
with System;
with Interfaces.C;
with Dl;
with Ada.Exceptions;
with Ada.Unchecked_Conversion;
procedure main is
use Ada.Text_IO;
use System;
use Ada.Exceptions;
handle : System.Address := System.Null_Address;
path : constant String := "./libexample.so";
-- === 함수 포인터 관련 선언 ===
func_addr : System.Address;
type my_function_access is access function (Arg : Interfaces.C.int) return Interfaces.C.int;
pragma Convention (C, my_function_access);
function to_my_function_access is new Ada.Unchecked_Conversion (
Source => System.Address, Target => my_function_access);
my_func_pointer : my_function_access := null;
result : Interfaces.C.int;
-- === 전역 변수 포인터 관련 선언 ===
var_addr : System.Address;
type int_access is access all Interfaces.C.int;
function to_int_access is new Ada.Unchecked_Conversion (
Source => System.Address,
Target => int_access
);
global_var_pointer : int_access := null;
global_value : Interfaces.C.int;
begin
put_line ("Loading " & path);
handle := dl.open (path, dl.RTLD_LAZY);
put_line ("Success: loaded " & path);
-- === 함수 심볼 가져오기 ===
put_line ("Looking up symbol 'my_function'");
func_addr := dl.get_symbol (handle, "my_function");
put_line ("Success: got address for 'my_function'");
my_func_pointer := to_my_function_access (func_addr);
-- === 전역 변수 심볼 가져오기 ===
put_line ("Looking up symbol 'my_global_variable'");
var_addr := dl.get_symbol (handle, "my_global_variable");
put_line ("Success: got address for 'my_global_variable'");
global_var_pointer := to_int_access(var_addr);
-- === 전역 변수 접근 (읽기) ===
global_value := global_var_pointer.all;
put_line ("Initial value of my_global_variable: " & Interfaces.C.int'Image (global_value));
-- === 함수 호출 (전역 변수를 수정할 수 있음) ===
put_line ("Calling my_function(10)...");
result := my_func_pointer (10);
put_line ("Result from my_function(10): " & Interfaces.C.int'Image (result));
-- === 전역 변수 다시 접근 (읽기) - 함수 호출 후 변경 확인 ===
global_value := global_var_pointer.all;
put_line ("Value of my_global_variable after function call: " & Interfaces.C.int'Image (global_value));
-- === 전역 변수 접근 (쓰기) - Ada 코드에서 값 변경 ===
put_line ("Setting my_global_variable to 777 from Ada...");
global_var_pointer.all := 777;
global_value := global_var_pointer.all;
put_line ("Value of my_global_variable after Ada write: " & Interfaces.C.int'Image (global_value));
-- 정리 (라이브러리 닫기)
if handle /= System.Null_Address then
put_line ("Closing library");
dl.close (handle);
end if;
exception
when e : others =>
put_line ("An unexpected error occurred: " & Exception_Name(e));
put_line ("Details: " & Exception_Information (e));
if handle /= System.Null_Address then
put_line("Attempting to close library gracefully...");
dl.close(handle);
end if;
end main;
6. 빌드 스크립트 (Makefile
)
이 모든 파일들을 올바른 순서로 컴파일하고 링크하는 과정을 자동화합니다.
# Makefile
# 컴파일러 설정 (자신의 환경에 맞게 수정)
CC = gcc
GNATMAKE = gnatmake
# 최종 목표: main 실행 파일과 libexample.so 공유 라이브러리
all: main libexample.so
# C 공유 라이브러리 빌드
libexample.so: libexample.c
$(CC) -shared -fPIC libexample.c -o libexample.so
# C 헬퍼 오브젝트 파일 빌드
dl-open.o: dl-open.c
$(CC) -c dl-open.c -o dl-open.o
# Ada 소스 컴파일 및 링크
main: main.adb dl.adb dl.ads dl-open.o
$(GNATMAKE) main.adb -largs dl-open.o -ldl
# 정리
clean:
rm -f *.o *.ali main libexample.so b~*
중요: gnatmake
의 -largs
옵션은 링커에게 추가 인자를 전달합니다. dl-open.o
를 함께 링크하고, -ldl
을 통해 시스템의 dl
라이브러리(dlopen
, dlsym
등이 포함된)를 링크하도록 지시합니다.
빌드 및 실행
위 파일들을 모두 저장했다면, 터미널에서 다음 명령어를 실행하세요.
1. 빌드하기
make
Makefile
에 정의된 규칙에 따라 libexample.so
와 main
실행 파일이 생성됩니다.
2. 실행하기
./main
예상 실행 결과
Loading ./libexample.so
Success: loaded ./libexample.so
Looking up symbol 'my_function'
Success: got address for 'my_function'
Looking up symbol 'my_global_variable'
Success: got address for 'my_global_variable'
Initial value of my_global_variable: 123
Calling my_function(10)...
(C) my_function called with 10
(C) Current my_global_variable = 123
(C) New my_global_variable = 133
Result from my_function(10): 100
Value of my_global_variable after function call: 133
Setting my_global_variable to 777 from Ada...
Value of my_global_variable after Ada write: 777
Closing library
결론
이처럼 전체 소스 코드와 빌드 과정을 통해, 우리는 C의 저수준 동적 라이브러리 인터페이스를 Ada의 타입 시스템과 예외 처리의 보호 아래로 가져왔습니다. 이 래퍼 패턴을 사용하면 복잡한 C 라이브러리 연동 프로젝트도 훨씬 안정적이고 유지보수하기 쉬운 구조로 만들 수 있습니다.