C API 래핑 패턴의 비교 분석
Ada의 예외를 C 언어의 에러 코드로 변환하기 위한 '예외 장벽(Exception Barrier)'을 구현하는 방법은 크게 세 가지가 있습니다. 각 방식의 장단점과 실제 구현 코드를 분석하여 Clair 라이브러리에 최적화된 아키텍처를 도출합니다.
1. 예외 매퍼 패턴 (Exception Mapper Pattern)
C API 함수 내부에서 비즈니스 로직을 직접 호출하고, exception 블록에서 예외를 처리하는 방식입니다. 이 패턴은 구현 방식에 따라 두 가지 형태로 나뉩니다.
- 인라인 핸들러 방식: 각 함수마다
when Clair.Exceptions.X => return -1;과 같이 개별 예외를 나열하는 방식. - 중앙 매퍼 호출 방식:
when others를 통해 모든 예외를 잡은 뒤, 중앙 유틸리티 함수(Map_Exception)로 넘기는 방식.
1.1 구현 코드 예시
[중앙 매퍼 함수 구현 (Clair.API_Utils)]
function Map_Exception (Occ : Exception_Occurrence) return Interfaces.C.int is
Id : Exception_Id := Exception_Identity (Occ);
begin
-- 1. Clair 논리 에러 체크 (가장 빈번한 것부터)
if Id = Clair.Exceptions.Display_Init_Error'Identity then
return Clair.Errors.Err_Display_Init;
elsif Id = Clair.Exceptions.Library_Load_Error'Identity then
return Clair.Errors.Err_Library_Load;
-- 2. 시스템 에러 (Errno) 체크
-- 필요하다면 Clair.Errno 패키지의 예외들과 비교
elsif Id = Clair.Errno.ENOMEM'Identity then
return Clair.Errno.ENOMEM_Val; -- (상수로 정의된 값)
-- 3. Ada 런타임 에러
elsif Id = Constraint_Error'Identity then
return Clair.Errors.Err_Invalid_Arg;
else
-- 로그를 남기거나 디버깅 정보를 출력할 수 있음
return Clair.Errors.Err_Unknown;
end if;
end Map_Exception;
[C API 함수 구현부]
-- C API 함수 내부
function C_Do_Work (...) return int is
begin
-- 내부 로직 직접 호출 (Zero Overhead)
Do_Internal(...); -- [Call Instruction]
return 0; -- [Move + Ret]
exception
-- 예외 발생 시 중앙 매퍼 호출
when E : others =>
return Map_Exception(E); -- [Call Instruction]
end C_Do_Work;
[Ruby 자동 생성 템플릿 예시]
# Ruby Generator Template Concept
template = <<-ADA
function #{c_name} (#{args}) return Interfaces.C.int
with Export, Convention => C, External_Name => "#{c_name}";
is
begin
-- 1. 내부 로직 호출 (Zero Overhead)
Clair.Core.#{ada_name} (#{converted_args});
return Clair.Errors.Success;
exception
-- 2. 예외 발생 시 중앙 매퍼 호출 (최소 코드량)
when E : others =>
return Clair.API_Utils.Map_Exception (E);
end #{c_name};
ADA
장점
- 제로 오버헤드: 정상 실행 시 함수 포인터나 객체 생성이 없어 C 함수와 동일한 성능을 냅니다.
- 최적화된 바이너리: 각 함수에 매퍼 호출 코드만 추가되므로 바이너리 크기가 가장 작습니다.
- 자동화 최적: 구조가 단순하여 위와 같은 스크립트로 자동 생성하기에 가장 적합합니다.
단점
- 보일러플레이트: 자동화를 하지 않을 경우, 함수마다
exception 블록을 작성해야 합니다.
2. 로컬 캡처 / Thunk 패턴 (Local Capture / Thunk Pattern)
Ada의 중첩 프로시저(Nested Procedure) 기능을 활용하여 로직을 감싸고(Bridge), 이를 중앙 실행기(Run_Safe)에 전달하는 방식입니다.
2.1 구현 코드 예시
with Interfaces.C;
with Clair.API_Utils;
with Clair.Core; -- 실제 로직이 있는 패키지
function C_Do_Work (X : Interfaces.C.int; Y : Interfaces.C.int) return Interfaces.C.int
with Export, Convention => C, External_Name => "clair_do_work";
is
-- [상태 변수] 로직 수행 결과를 저장할 변수
Result_Val : Integer := 0;
------------------------------------------------------------
-- [Thunk] 지역 프로시저 (Bridge)
-- 상위 함수의 인자(X, Y)와 변수(Result_Val)에 접근 가능
------------------------------------------------------------
procedure Bridge is
begin
-- C 타입 -> Ada 타입 변환 및 실제 함수 호출
Result_Val := Clair.Core.Do_Work (Integer(X), Integer(Y));
end Bridge;
begin
-- [실행] Bridge의 Access를 넘겨서 실행
-- Run_Safe는 예외 발생 여부(에러 코드)를 반환함
declare
Status : constant Interfaces.C.int := Clair.API_Utils.Run_Safe (Bridge'Access);
begin
if Status /= Clair.Errors.Success then
return Status;
end if;
-- 예외가 없다면 실제 로직의 결과 반환
return Interfaces.C.int(Result_Val);
end;
end C_Do_Work;
장점
- 간결한 구현: 파라미터 구조체 없이도 외부 변수를 캡처할 수 있어 코드가 직관적입니다.
- 정책의 중앙화: 모든 실행 흐름이
Run_Safe를 통과하므로 예외 정책 관리가 용이합니다.
단점
- 간접 호출 비용: Thunk를 경유하므로 미세한 오버헤드가 발생합니다.
- 보안 주의 및 컴파일러 의존성: Thunk의
Access를 C 함수 포인터로 직접 넘기면 스택에 코드를 생성하는 트램펄린(Trampoline)이 발생하여 보안(NX Bit) 위험이 있습니다. 또한, Ada 내부 호출이 안전하다는 점(트램펄린 미생성) 역시 컴파일러의 구현 방식(예: GNAT의 Fat Pointer 사용 여부)에 의존적이므로, 엄격한 보안과 이식성이 요구되는 환경에서는 주의가 필요합니다.
3. 파라미터 레코드 + 접근자 패턴 (Parameter Record Pattern)
모든 매개변수를 명시적인 레코드(record) 타입으로 정의하고, 이를 패키지 레벨의 정적 함수에 전달하는 방식입니다.
3.1 구현 코드 예시
type Params is record
X : Integer;
Y : Integer;
end record;
type Params_Access is access all Params;
type Param_Thunk is access function (P : Params_Access) return Interfaces.C.int;
function Handle_Exceptions_Param (F : Param_Thunk; P : Params_Access) return Interfaces.C.int is
begin
return F.all (P);
exception
when others =>
Set_Last_Error (-999, Exception_Message (Ada.Exceptions.Others));
return -999;
end Handle_Exceptions_Param;
function Do_Work_With_Params (P : Params_Access) return Interfaces.C.int is
begin
return Do_Work_Internal (P.X, P.Y);
end Do_Work_With_Params;
function clair_do_work (x : Interfaces.C.int; y : Interfaces.C.int) return Interfaces.C.int is
P : aliased Params := (X => Integer(x), Y => Integer(y));
begin
return Handle_Exceptions_Param (Do_Work_With_Params'Access, P'Access);
end clair_do_work;
장점
- 완벽한 보안성: 중첩 함수를 사용하지 않으므로 트램펄린 생성 가능성이 0%입니다.
- 명시적 관리: 데이터와 실행 코드가 분리되어 비동기 작업 처리에 유리합니다.
단점
- 높은 작성 비용: 함수마다 전용 레코드와 접근자 타입을 정의해야 하므로 코드량이 매우 많아집니다.
결론: Clair 라이브러리의 선택
Clair 라이브러리는 **성능(Zero Overhead)**과 안전성(Safety), 그리고 **개발 생산성(Productivity)**을 모두 만족하기 위해 '중앙 집중식 예외 매퍼(Centralized Exception Mapper)' 전략을 논리적 기반으로 채택합니다.
실제 구현에서는 이 전략을 바탕으로 반복적인 코드 작성을 피하고 타입 안전성을 확보하기 위해 'N-Arity 제네릭 래퍼(N-Arity Generic Wrapper)' 패턴을 사용하여 구현합니다. 이는 1번 패턴의 성능 이점과 2번 패턴의 코드 간결성을 동시에 달성하는 최적의 해법입니다.
Clair 라이브러리 예외(에러) C 바인딩 전략
1.1 개요
Ada로 작성된 라이브러리인 Clair를 C 언어 환경에서 사용하기 위해서는 **언어 경계(Language Boundary)**를 넘나드는 견고한 인터페이스 설계가 필수적입니다. Ada의 예외(Exception)는 C 언어가 이해할 수 없으므로, 이를 C가 처리할 수 있는 정수형 에러 코드(Error Code)로 변환하여 반환해야 합니다.
본 장에서는 성능(Performance), 안전성(Safety), 그리고 유지보수성(Maintainability)을 고려한 **'중앙 집중식 예외 매퍼(Centralized Exception Mapper)'**와 'N-Arity 제네릭 래퍼(N-Arity Generic Wrapper)' 패턴을 소개합니다.
1.2 핵심 설계 원칙
Clair 라이브러리의 C API 바인딩은 다음 세 가지 원칙을 따릅니다.
- 예외 장벽(Exception Barrier)의 수립: Ada의 예외가 처리되지 않은 채 C 영역(Foreign Frame)으로 전파될 경우, Ada 런타임은 프로그램을 즉시 **종료(Termination)**시킵니다. 라이브러리 함수 하나가 실패했다고 해서 전체 애플리케이션이 종료되는 것을 막기 위해, 경계에서 모든 예외를 포착해야 합니다.
- 제로 오버헤드(Zero Overhead): 정상 실행 경로(Happy Path)에서는 불필요한 객체 생성이나 동적 디스패치(Dynamic Dispatch)를 피하고, C 함수 호출과 동일한 성능을 보장해야 합니다.
- 보안 안전성(Security Safety): 스택 실행 권한(NX Bit)을 침해하는 트램펄린(Trampoline) 생성을 원천적으로 방지하기 위해, Ada 내부 규약을 따르는 정적 바인딩(Static Binding) 방식을 사용합니다.
1.3 패키지 구조와 역할
예외와 에러 코드를 명확히 분리하기 위해 다음과 같이 패키지를 구성합니다.
Clair.Errno: 운영체제 시스템 에러 코드와 이에 대응하는 Ada 예외를 정의합니다. (자동 생성됨)Clair.Exceptions: Clair 라이브러리 내부 로직 흐름을 제어하기 위한 고수준 Ada 예외 객체입니다.Clair.Errors: C API가 반환할 인터페이스용 정수형 상수(Status Code)입니다.Clair.API_Utils: C API 경계에서 발생하는 예외를 에러 코드로 변환하는 중앙 실행기입니다.
1.4 구현 상세
1.4.1 중앙 실행기 (Clair.API_Utils)
이 패키지는 모든 C API 함수가 공유하는 단 하나의 실행기를 제공합니다. 개별 API 함수마다 예외 처리 로직을 중복 작성하는 대신, 이 실행기가 예외 변환을 전담합니다.
with Ada.Exceptions;
with Interfaces.C;
with Clair.Errors;
with Clair.Exceptions;
with Clair.Errno;
package body Clair.API_Utils is
-- 모든 C API가 공유하는 예외 처리 실행기
function Run_Protected (Action : Action_Proc) return Interfaces.C.int is
begin
-- 1. 실제 동작 실행 (Zero Overhead: 단순 간접 호출)
Action.all;
-- 2. 성공 시 0 반환
return Clair.Errors.Success;
exception
------------------------------------------------------------
-- 3. 중앙 집중식 예외 매핑 (Native Handler Dispatch)
------------------------------------------------------------
-- (A) Clair 라이브러리 논리 에러
when Clair.Exceptions.Display_Init_Error =>
return Clair.Errors.Err_Display_Init;
when Clair.Exceptions.Library_Load_Error =>
return Clair.Errors.Err_Library_Load;
-- (B) 시스템 에러 (Errno 패키지와 연동)
when Clair.Errno.ENOMEM =>
return Clair.Errno.ENOMEM_Val;
-- (C) Ada 런타임 에러
when Constraint_Error =>
return Clair.Errors.Err_Invalid_Arg;
when Storage_Error =>
return Clair.Errors.Err_System_Fatal;
-- (D) 그 외 예측 불가능한 에러
when others =>
return Clair.Errors.Err_Unknown;
end Run_Protected;
end Clair.API_Utils;
1.4.2 N-Arity 제네릭 래퍼 (Clair.Wrappers)
인자의 개수(Arity) 별로 제네릭 함수를 미리 정의합니다. 이 방식은 타입 안전성을 보장하며, 내부적으로 Thunk를 생성하여 Run_Protected에게 전달합니다. Ada 내부 호출 규약(Convention Ada)을 따르므로 트램펄린이 생성되지 않아 안전합니다.
-- [Clair.Wrappers.ads]
package Clair.Wrappers is
-- 인자가 2개인 경우의 제네릭 템플릿
generic
type Arg1_Type is <>;
type Arg2_Type is <>;
with procedure Action (A1 : Arg1_Type; A2 : Arg2_Type);
function Wrap_2 (A1 : Arg1_Type; A2 : Arg2_Type) return Interfaces.C.int;
end Clair.Wrappers;
-- [Clair.Wrappers.adb]
package body Clair.Wrappers is
function Wrap_2 (A1 : Arg1_Type; A2 : Arg2_Type) return Interfaces.C.int is
-- Thunk: 인자를 캡처하여 전달 (Ada 내부에서만 사용되므로 안전)
procedure Bridge is
begin
Action (A1, A2);
end Bridge;
begin
-- 예외 처리 없이 실행기에 위임
return Clair.API_Utils.Run_Protected (Bridge'Access);
end Wrap_2;
end Clair.Wrappers;
1.4.3 사용자 API 구현 예시 (clair_do_work)
라이브러리 개발자는 복잡한 예외 처리 로직을 매번 작성할 필요 없이, 제네릭 인스턴스화를 통해 래퍼를 생성하고 사용합니다.
with Clair.Wrappers;
with Clair.Core;
package body Clair.Work is
-- 1. 내부 비즈니스 로직 (Ada 스타일)
procedure Do_Internal (X, Y : Integer) is
begin
Clair.Core.Do_Work (X, Y);
end Do_Internal;
-- 2. 래퍼 생성 (타입과 함수만 지정)
-- 개발자가 명시적으로 이 래퍼를 생성함으로써 안전한 예외 처리가 적용됩니다.
function Safe_Do_Work is new Clair.Wrappers.Wrap_2
(Arg1_Type => Integer,
Arg2_Type => Integer,
Action => Do_Internal);
-- 3. C API Export
function C_Do_Work (X, Y : Interfaces.C.int) return Interfaces.C.int
with Export, Convention => C, External_Name => "clair_do_work";
is
begin
-- C 타입을 Ada 타입으로 변환하여 안전한 래퍼 호출
return Safe_Do_Work (Integer(X), Integer(Y));
end C_Do_Work;
end Clair.Work;
1.5 결론
이 아키텍처는 다음과 같은 이점을 제공합니다.
- 안전성 (Correctness): 이 패턴을 적용하면 Ada 런타임에 의한 프로그램 강제 종료를 방지하고, 모든 예외를 C가 이해할 수 있는 에러 코드로 안전하게 변환할 수 있습니다.
- 생산성 (Productivity):
Clair.Wrappers의 제네릭을 활용함으로써 반복적인 코드 작성을 최소화합니다. - 성능 (Performance): 정상 실행 시에는 추가적인 비용이 없는 함수 호출 성능을 제공합니다.
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.