강력한 안전성과 신뢰성을 자랑하는 Ada 언어로 C 라이브러리를 작성하는 방법에 대해 알아보겠습니다. 특히 C 프로그램에 메인(main) 함수가 있을 때 발생하는 Ada 런타임 초기화 문제를 해결하는 방법까지 포함하여 전체 과정을 단계별로 살펴보겠습니다.
왜 Ada로 C 라이브러리를 만들까요?
Ada로 C 라이브러리를 만드는 주된 이유는 Ada의 높은 신뢰성, 안전성, 유지보수성 등의 장점을 활용하여 시스템의 특정 부분을 더 견고하게 만들거나, 기존 Ada 코드를 재사용하거나, 점진적으로 Ada를 도입하기 위해서입니다. 즉, C의 광범위한 생태계와 성능, 그리고 Ada의 안정성이라는 두 언어의 장점을 조합하여 사용하는 전략이라고 볼 수 있습니다.
준비 사항:
- 기본적인 C, Ada 언어 지식
- GNAT Ada 컴파일러 (GCC 포함)
- C 컴파일러 (GCC 또는 Clang 권장)
make
빌드 도구- 텍스트 에디터
과정 요약:
- Ada 코드 작성 (
.ads
,.adb
) 및 C 인터페이스 정의 - C 헤더 파일 작성 (
.h
) - C 호출 프로그램 작성 (
.c
) - Ada 런타임 초기화 문제 이해 및 해결
Makefile
을 이용한 빌드 자동화- 실행 및 확인
1단계: Ada 코드 작성 및 C 인터페이스 정의
먼저 C에서 호출할 간단한 Ada 함수(프로시저)를 만들어 보겠습니다. “Hello, World!”를 출력하는 기능입니다.
hello.ads
(Ada 명세 파일)
-- 파일명: hello.ads
-- 'Hello' 패키지: C에서 호출될 기능을 정의합니다.
package Hello is
-- C에서 호출할 print_hello 프로시저를 선언합니다.
procedure print_hello -- C에서 사용할 이름이랑 같을 필요는 없습니다.
with -- export 관련 정보를 aspect 구문으로 지정 (Ada 2012/2022 스타일)
export => true, -- 외부로 내보냅니다.
convention => c, -- C 호출 규약을 따릅니다.
external_name => "print_hello"; -- C 코드에서 사용할 함수 이름입니다.
end Hello;
Code language: Ada (ada)
hello.adb
(Ada 본체 파일)
-- 파일명: hello.adb
-- print_hello 프로시저의 실제 구현
with Ada.Text_IO;
package body Hello is
procedure print_hello is
begin
-- Ada.Text_IO를 사용하여 콘솔에 메시지를 출력합니다.
Ada.Text_IO.put_line ("Hello, World! from Ada");
end print_hello;
end Hello;
Code language: CSS (css)
2단계: C 헤더 파일 작성 (hello.h
)
C 코드에서 Ada 함수(print_hello
)를 인식하고 올바르게 호출하려면 해당 함수의 원형(prototype)을 선언한 헤더 파일이 필요합니다.
/* 파일명: hello.h */
#ifndef HELLO_H
#define HELLO_H
/* C++ 호환성을 위한 처리 */
#ifdef __cplusplus
/* C++ 컴파일러가 C 방식으로 함수 이름을 처리하도록 합니다. (name mangling 방지) */
extern "C" {
#endif
/*
* Ada 코드(hello.ads)에서 external_name으로 지정한 이름과
* 동일한 이름으로 함수 원형을 선언합니다.
* Ada의 procedure print_hello는 파라미터와 반환값이 없으므로,
* C에서는 void print_hello(void) 형태가 됩니다.
*/
void print_hello (void);
#ifdef __cplusplus
}
#endif
#endif /* HELLO_H */
Code language: C++ (cpp)
3단계: C 프로그램 작성 (app.c
)
이제 Ada로 만든 함수를 호출하는 간단한 C 메인 프로그램을 작성합니다.
/* 파일명: app.c */
#include <stdio.h>
#include "hello.h" /* Ada 함수 선언을 위한 헤더 */
int main ()
{
printf ("C: Ada 함수를 호출합니다...\n");
/* Ada로 구현된 함수 호출 */
print_hello ();
printf ("C: Ada 함수에서 돌아왔습니다.\n");
return 0;
}
Code language: C++ (cpp)
4단계: Ada 런타임 초기화 문제 이해 및 해결
문제점: 위 app.c
코드를 지금 상태에서 Ada로 만든 C 라이브러리와 링크하여 실행하면, 높은 확률로 print_hello
함수 내부의 Ada.Text_IO.put_line
호출 시 런타임 오류 (예: ADA.IO_EXCEPTIONS.STATUS_ERROR: file not open
)가 발생합니다.
이유: C 프로그램이 메인일 때는 Ada 런타임 환경이 자동으로 초기화되지 않습니다. Ada 코드가 올바르게 동작하기 위해 필요한 환경(메모리 관리, 태스킹, 예외 처리, 표준 입출력 설정 등)을 설정하는 adainit()
함수와, 프로그램 종료 시 이를 정리하는 adafinal()
함수가 호출되어야 합니다. Ada.Text_IO
는 특히 이 초기화 과정에서 표준 출력 스트림이 준비되는 것에 의존합니다.
Ada 런타임 초기화 및 종료에 대한 자세한 내용은 GNAT User’s Guide의 “Binding with Non-Ada Main Programs” 섹션에서 확인할 수 있습니다.
해결 방법:
수동 호출 (비권장):
app.c
코드에서 main
함수 시작 시 adainit()
을, 종료 시 adafinal()
을 명시적으로 호출하는 방법입니다. 가장 확실하지만 C 코드를 수정해야 하고 라이브러리 사용자가 이 사실을 알아야 합니다.
자동 호출 (GCC/Clang 확장 기능 사용 – 권장):
C 컴파일러의 constructor
/ destructor
속성을 이용하여 main
함수 실행 전/후에 자동으로 adainit
/ adafinal
을 호출하는 방법입니다. C 코드를 수정할 필요가 없고 사용이 편리합니다.
아래 내용으로 ada-init-fini.c
파일을 생성합니다.
// 파일 이름: ada-init-fini.c
extern void adainit (void);
extern void adafinal (void);
// main() 실행 전에 자동으로 호출됨
__attribute__((constructor))
static void ada_runtime_init (void) { adainit(); }
// main() 종료 후 자동으로 호출됨
__attribute__((destructor))
static void ada_runtime_fini (void) { adafinal(); }
Code language: C++ (cpp)
이 파일을 컴파일하여 얻은 ada-init-fini.o
파일을 공유 라이브러리에 포합시킵니다. 안타깝게도 정적 라이브러리에서는 위 방법은 통하지 않기 때문에 최종 실행 파일 링크 시 포함시키거나 main()
함수 내에서 adainit()
와 adafinal()
을 호출하셔야 됩니다.
5단계: Makefile
을 이용한 빌드 자동화 (POSIX 규격)
이제 Ada 코드 컴파일, 바인딩, 라이브러리 생성, C 코드 컴파일, 최종 링크까지 모든 과정을 처리하는 Makefile
을 작성합니다.
# Makefile (POSIX Compliant for FreeBSD - Static & Shared with Auto Init/Fini)
# --- Compiler and Tools ---
CC = gcc
AR = ar
RM = rm -f
GNATMAKE = gnatmake
GNATBIND = gnatbind
# --- Source Files ---
ADS_SOURCES = hello.ads
ADB_SOURCES = hello.adb
C_SOURCES = app.c ada-init-fini.c # 자동 초기화 코드 포함
C_HEADERS = hello.h
# Ada 유닛 이름
BASE = hello
# --- Object Files ---
ADA_OBJS = hello.o
APP_OBJ = app.o
INIT_FINI_OBJ = ada-init-fini.o # 자동 초기화 오브젝트
C_OBJS = $(APP_OBJ) $(INIT_FINI_OBJ) # 링크 시 사용될 C 오브젝트 목록
B_OBJ = b~$(BASE).o
ALI_FILE = $(BASE).ali
BIND_SOURCES_ADS = b~$(BASE).ads
BIND_SOURCES_ADB = b~$(BASE).adb
# --- Library & Executable Names ---
STATIC_LIB = libhello.a
SHARED_LIB = libhello.so
EXE_STATIC = app-static
EXE_SHARED = app # 공유 라이브러리 사용 실행 파일
# --- Ada Runtime Libraries ---
# !!! 중요: Ada 런타임 라이브러리 경로 설정 !!!
# 아래 GNAT_LIB_DIR 경로는 시스템 환경(OS, GNAT 설치 방식, GNAT 버전 등)에 따라
# 크게 달라질 수 있으므로, 반드시 자신의 환경에 맞게 확인하고 수정해야 합니다.
# 예시 경로는 FreeBSD 환경의 특정 GNAT 버전을 기준으로 작성되었습니다.
#
# 자신의 시스템에서 올바른 Ada 라이브러리 경로('adalib' 디렉토리)를 찾는 방법:
# 1. 터미널에서 `gnatls -v` 명령어를 실행합니다.
# 2. 출력 내용 중 'Object Search Path:' 또는 유사한 섹션을 찾습니다.
# 3. 해당 목록에 포함된 경로 중 'adalib' 디렉토리를 포함하는 경로를 찾습니다.
# (예: /usr/lib/gcc/x86_64-linux-gnu/11/adalib, /usr/local/gnat/lib/...)
# 4. 찾은 'adalib' 디렉토리의 전체 경로를 아래 GNAT_LIB_DIR에 설정합니다.
#
# (FreeBSD 참고) `pkg info -l gnat13 | grep adalib` 등으로 경로 확인 가능
GNAT_LIB_DIR = /usr/local/gnat13/lib/gcc/x86_64-portbld-freebsd14.2/13.2.0/adalib
GNAT_LIBS_STATIC = $(GNAT_LIB_DIR)/libgnat.a $(GNAT_LIB_DIR)/libgnarl.a
GNAT_LIBS_SHARED = -L$(GNAT_LIB_DIR) -lgnat
# --- Flags ---
ADACFLAGS = -fPIC
# 헤더 파일이 현재 디렉토리에 있으므로 -I. 추가 (선택 사항)
CFLAGS = -fPIC -I.
LDFLAGS = -Wl,--as-needed
# 예시를 편리하게 보여주기 위해 -rpath 옵션을 주었습니다.
# 실제 운용 환경에서는 적절히 수정할 필요가 있습니다.
LINK_ARGS_SHARED = -Wl,-rpath=$(GNAT_LIB_DIR):.
all: $(STATIC_LIB) $(SHARED_LIB) $(EXE_STATIC) $(EXE_SHARED)
static: $(EXE_STATIC)
shared: $(EXE_SHARED)
# --- Executable Linking ---
# 링크 시 자동 초기화 오브젝트 포함
$(EXE_STATIC): $(C_OBJS) $(STATIC_LIB)
@echo "Linking $@ (static using gcc)..."
$(CC) $(CFLAGS) $(C_OBJS) -o $@ $(LDFLAGS) \
$(STATIC_LIB) $(GNAT_LIBS_STATIC)
$(EXE_SHARED): $(C_OBJS) $(SHARED_LIB)
@echo "Linking $@ (shared using gcc)..."
$(CC) $(CFLAGS) $(C_OBJS) -o $@ $(LDFLAGS) \
-L. -lhello $(GNAT_LIBS_SHARED) $(LINK_ARGS_SHARED)
# --- Library Creation ---
$(STATIC_LIB): $(ADA_OBJS) $(B_OBJ)
@echo "Creating static library $@"
$(AR) rcs $@ $(ADA_OBJS) $(B_OBJ)
# 공유 라이브러리 생성 (gcc 수동 링크 - gnatmake 문제 우회)
$(SHARED_LIB): $(ADA_OBJS) $(B_OBJ)
@echo "Creating shared library $@ (manual gcc)..."
$(CC) -shared $(CFLAGS) -o $@ $(ADA_OBJS) $(B_OBJ) \
$(GNAT_LIBS_SHARED) -Wl,-soname,$(SHARED_LIB) $(LINK_ARGS_SHARED)
# --- Ada Binding Step ---
$(BIND_SOURCES_ADS) $(BIND_SOURCES_ADB): $(ALI_FILE)
$(GNATBIND) -n $(ALI_FILE)
$(B_OBJ): $(BIND_SOURCES_ADB) $(BIND_SOURCES_ADS)
$(GNATMAKE) $(ADACFLAGS) -c $(BIND_SOURCES_ADB) -o $@
# --- Object File Dependencies ---
$(ALI_FILE): $(ADA_OBJS)
# --- Suffix Rules ---
.SUFFIXES: # 이미 존재하는 규칙을 초기화합니다.
.SUFFIXES: .c .o .adb .ads .ali
# .c.o 규칙: app.c 와 ada-init-fini.c 모두 처리
.c.o: $(C_HEADERS) Makefile
$(CC) $(CFLAGS) -c $< -o $@
# .adb.o 규칙: Ada 컴파일 (Makefile 변경 시 재컴파일 보장 포함)
.adb.o: $(ADS_SOURCES) Makefile
$(GNATMAKE) $(ADACFLAGS) -c $<
# --- Utility Targets ---
run: run-static
run-static: $(EXE_STATIC)
@echo "--- Running $(EXE_STATIC) (static) ---"
./$(EXE_STATIC)
@echo "--- Done ---"
run-shared: $(EXE_SHARED)
@echo "--- Running $(EXE_SHARED) (shared) ---"
./$(EXE_SHARED)
@echo "--- Done ---"
clean:
@echo "Cleaning..."
$(RM) $(APP_OBJ) $(INIT_FINI_OBJ) $(ADA_OBJS) $(B_OBJ) \
$(ALI_FILE) $(BIND_SOURCES_ADS) $(BIND_SOURCES_ADB) \
$(STATIC_LIB) $(SHARED_LIB) $(EXE_STATIC) $(EXE_SHARED) \
*.adc b~hello.ali
# --- Targets ---
.PHONY: all static shared clean run run-static run-shared
Code language: Makefile (makefile)
- 주의:
Makefile
상단의GNAT_LIB_DIR
경로는 실제 GNAT 설치 환경에 맞게 확인하고 수정해야 합니다. C 컴파일 시 헤더 파일(hello.h
)이 현재 디렉토리에 있으므로CFLAGS
에-I.
를 추가할 수도 있지만, 보통은 기본적으로 현재 디렉토리를 검색하므로 생략 가능합니다.C_HEADERS
변수명을hello.h
로 수정했습니다.
6단계: 실행 및 확인
1. 지금까지 작성한 6개의 파일
hello.ads -- 명세 파일
hello.adb -- 본체 파일
hello.h -- 헤더 파일
app.c -- 실행 파일을 만들기 위해 main() 함수를 포함
ada-init-fini.c -- 자동 초기화용 파일
Makefile -- 빌드용 파일
Code language: CSS (css)
을 편의상 한 디렉토리에 저장합니다.
2. 터미널에서 해당 디렉토리로 이동합니다.
3. make clean
명령으로 이전 빌드 결과물을 정리합니다.
4. make
명령으로 전체 빌드를 수행합니다.
5. make run-static
또는 ./app-static
명령으로 정적 링크 버전을 실행합니다.
6. make run-shared
또는 ./app
명령으로 공유 라이브러리 버전을 실행합니다.
예상 출력 (두 버전 모두 동일):
C: Ada 함수를 호출합니다...
Hello, World! from Ada
C: Ada 함수에서 돌아왔습니다.
Code language: JavaScript (javascript)
이제 런타임 오류 없이 C 프로그램에서 Ada 함수가 성공적으로 호출되는 것을 볼 수 있습니다.
실전 배포를 위한 추가 고려 사항
지금까지 우리는 Ada로 C 라이브러리를 만들고 C 프로그램에서 호출하는 기본적인 과정을 살펴보았습니다. 이 예제는 개념을 이해하는 데 중점을 두었지만, 실제 운용 환경에서 사용할 라이브러리나 애플리케이션을 개발하고 배포하기 위해서는 다음과 같은 추가적인 사항들을 고려해야 합니다.
- 최적화 옵션 (Optimization): 개발 중에는 디버깅 편의성을 위해 최적화 옵션을 사용하지 않는 경우가 많습니다. 하지만 최종 배포 버전에서는 성능 향상을 위해 Ada 컴파일(
ADACFLAGS
)과 C 컴파일(CFLAGS
) 모두에-O1
또는-O2
와 같은 적절한 최적화 레벨을 적용해야 합니다. - 정적 링킹 vs. 동적 링킹 (static vs. dynamic linking): 배포 시나리오에 따라 정적 링킹(모든 코드를 실행 파일에 포함, 배포 간편, 크기 증가) 또는 동적 링킹(라이브러리 공유, 업데이트 용이, 외부 라이브러리 의존성 발생) 중 적합한 방식을 선택해야 합니다.
strip
명령어 사용: 최종 배포 버전에서는strip
명령어를 사용하여 실행 파일과 공유 라이브러리에서 디버깅 심볼을 제거하면 파일 크기를 줄일 수 있습니다. (단, 디버깅 정보는 사라집니다. 예:strip libhello.so
)- C 컴파일러 안전 옵션: C 코드를 컴파일하고 링크할 때
-fstack-protector-strong
,-D_FORTIFY_SOURCE=2
,-Wl,-z,relro,-z,now
와 같은 보안 강화 옵션 사용을 고려하세요. libgnat.so
배포 및 경로: 동적 링킹된 애플리케이션(app
)을 배포할 경우, 대상 시스템에 호환되는 버전의libgnat.so
(및 필요시libgnarl.so
)가 동적 링커가 찾을 수 있는 경로(예:/usr/lib
,/usr/local/lib
또는 실행 파일의rpath
에 지정된 경로)에 설치되어 있어야 합니다. 시스템 라이브러리로 설치되어 있지 않다면, 애플리케이션과 함께 번들링하고rpath
를 올바르게 설정하는 등의 방법이 필요합니다.- 라이선스 (license):
- GNAT 런타임: 일반적으로 GNAT Ada 컴파일러와 런타임 라이브러리(
libgnat
,libgnarl
)는 GPL 라이선스에 런타임 예외(runtime library exception) 조항이 적용된 라이선스 (GMGPL 또는 유사 명칭)를 따릅니다. 이 예외 조항 덕분에 여러분이 작성한 코드(독점 코드 포함)를 GNAT 런타임 라이브러리와 링크하여 배포하더라도, 여러분의 코드 전체를 GPL로 공개할 의무는 일반적으로 발생하지 않습니다. - 작성한 코드: 직접 작성한 Ada 코드(
hello.ads
,hello.adb
)와 C 코드(app.c
,ada-init-fini.c
)의 라이선스는 직접 결정해야 합니다 (예: Proprietary, MIT, Apache, GPL 등). - 배포 시 주의: 최종 결과물을 배포할 때는 포함된 GNAT 런타임 라이브러리의 라이선스 조건(예외 조항 포함)과 직접 작성한 코드의 라이선스 조건을 모두 확인하고 준수해야 합니다. 본 설명은 법률 자문이 아니므로, 상업적 배포 등 중요한 경우에는 반드시 라이선스 원문을 확인하시고 전문가의 검토를 받으시기 바랍니다.
- GNAT 런타임: 일반적으로 GNAT Ada 컴파일러와 런타임 라이브러리(
--as-needed
링커 옵션:Makefile
의 최종 실행 파일 링크 단계에-Wl,--as-needed
옵션을 추가했습니다 (LDFLAGS
변수).- 이 옵션은 링커가 공유 라이브러리(-l…)를 무조건 연결하는 것이 아니라, 실제로 프로그램 실행에 꼭 필요한 심볼이 있는 경우에만 해당 라이브러리를 연결하도록 설정합니다. 이를 통해 실행 파일이 불필요한 공유 라이브러리에 의존하지 않도록 하여, 의존성 목록을 깔끔하게 정리할 수 있습니다.
- 우리 예제에서는
app
이libhello
를 사용하고,libhello
는 Ada 런타임(libgnat
)을 필요로 하므로--as-needed
옵션이 있더라도libgnat.so
는 링크될 것입니다. - 주의: 만약 어떤 라이브러리가 명시적인 심볼 참조 없이 오직 부수 효과(예: constructor 실행)나 간접적인 의존성 때문에 필요하다면, 이 옵션을 사용할 경우 해당 라이브러리가 링크 대상에서 누락될 수 있습니다. 이로 인해 프로그램 실행 중에 예상치 못한 오류가 발생할 가능성이 있기 때문에, 이 옵션을 사용할 때는 반드시 충분하고 철저한 테스트가 필요합니다.
이러한 고려 사항들을 바탕으로 실제 환경에 맞는 빌드 및 배포 전략을 수립하는 것이 중요합니다.
답글 남기기