C와의 인터페이싱
C와의 인터페이싱 (Interfacing with C)
Ada는 C와 C++을 포함한 많은 언어로 작성된 코드와 인터페이스할 수 있어요. 이 절에서는 C와 인터페이스하는 방법을 다룰게요.
출처: C와의 인터페이싱 문서
본문
다중 언어 프로젝트 (Multi-language project)
기본적으로 gprbuild를 사용할 때는 Ada 소스 파일만 컴파일해요. C 파일도 함께 컴파일하려면 gprbuild가 사용하는 프로젝트 파일을 수정해야 해요. 다음 예시처럼 Languages 항목을 사용해요:
project Multilang is
for Languages use ("ada", "c");
for Source_Dirs use ("src");
for Main use ("main.adb");
for Object_Dir use "obj";
end Multilang;
타입 규약 (Type convention)
C 애플리케이션에 선언된 데이터 타입과 인터페이스하려면 해당 Ada 타입 선언에 Convention 애스펙트를 지정해요. 다음 예시에서 C 소스 파일에 선언된 C_Enum 열거형과 인터페이스해요:
procedure Show_C_Enum is
type C_Enum is (A, B, C)
with Convention => C;
-- C_Enum에 C 규약 사용
begin
null;
end Show_C_Enum;
C의 내장 타입과 인터페이스하려면 Interfaces.C 패키지를 사용해요. 이 패키지는 필요한 대부분의 타입 정의를 포함해요. 예를 들어:
with Interfaces.C; use Interfaces.C;
procedure Show_C_Struct is
type c_struct is record
a : int;
b : long;
c : unsigned;
d : double;
end record
with Convention => C;
begin
null;
end Show_C_Struct;
여기서는 C 구조체(C_Struct)와 인터페이스하고 C의 해당 데이터 타입(int, long, unsigned, double)을 사용해요. 다음이 C에서의 선언이에요:
struct c_struct
{
int a;
long b;
unsigned c;
double d;
};
외부 서브프로그램 (Foreign subprograms)
Ada에서 C 서브프로그램 호출 (Calling C subprograms in Ada)
C로 작성된 서브프로그램과 인터페이스할 때도 유사한 접근 방식을 사용해요. C 헤더 파일의 다음 선언을 고려해요:
int my_func (int a);
다음이 해당 C 정의예요:
#include "my_func.h"
int my_func (int a)
{
return a * 2;
}
Import 애스펙트를 사용해 이 코드를 Ada에서 인터페이스할 수 있어요. 예를 들어:
with Interfaces.C; use Interfaces.C;
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_C_Func is
function my_func (a : int) return int
with
Import => True,
Convention => C;
-- C에서 함수 'my_func'을 가져옴.
-- 이제 Ada에서 호출할 수 있음.
V : int;
begin
V := my_func (2);
Put_Line ("Result is " & int'Image (V));
end Show_C_Func;
원한다면 Ada 코드에서 다른 서브프로그램 이름을 사용할 수 있어요. 예를 들어 C 함수 Get_Value를 호출할 수 있어요:
with Interfaces.C; use Interfaces.C;
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_C_Func is
function Get_Value (a : int) return int
with
Import => True,
Convention => C,
External_Name => "my_func";
-- C에서 함수 'my_func'을 가져와
-- 'Get_Value'로 이름을 바꿈
V : int;
begin
V := Get_Value (2);
Put_Line ("Result is " & int'Image (V));
end Show_C_Func;
C에서 Ada 서브프로그램 호출 (Calling Ada subprograms in C)
C 애플리케이션에서 Ada 서브프로그램을 호출할 수도 있어요. 이는 Export 애스펙트로 해요. 예를 들어:
with Interfaces.C; use Interfaces.C;
package C_API is
function My_Func (a : int) return int
with
Export => True,
Convention => C,
External_Name => "my_func";
end C_API;
다음이 그 함수를 구현하는 해당 본문이에요:
package body C_API is
function My_Func (a : int) return int is
begin
return a * 2;
end My_Func;
end C_API;
C 쪽에서는 함수가 C로 작성된 것처럼 동일하게 처리해요: extern 키워드로 선언하기만 하면 돼요. 예를 들어:
#include <stdio.h>
extern int my_func (int a);
int main (int argc, char **argv) {
int v = my_func(2);
printf("Result is %d\n", v);
return 0;
}
외부 변수 (Foreign variables)
Ada에서 C 전역 변수 사용 (Using C global variables in Ada)
C 코드의 전역 변수를 사용하려면 서브프로그램과 같은 방법을 사용해요: 가져오려는 각 변수에 Import와 Convention 애스펙트를 지정해요.
이전 절의 예시를 재사용할게요. 함수(my_func)가 호출된 횟수를 세는 전역 변수(func_cnt)를 추가할 거예요:
extern int func_cnt;
int my_func (int a);
변수는 C 파일에서 선언되고 my_func에서 증가돼요:
#include "test.h"
int func_cnt = 0;
int my_func (int a)
{
func_cnt++;
return a * 2;
}
Ada 애플리케이션에서는 외부 변수를 참조하기만 하면 돼요:
with Interfaces.C; use Interfaces.C;
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_C_Func is
function my_func (a : int) return int
with
Import => True,
Convention => C;
V : int;
func_cnt : int
with
Import => True,
Convention => C;
-- test.c의 func_cnt 변수에
-- 접근할 수 있음
begin
V := my_func (1);
V := my_func (2);
V := my_func (3);
Put_Line ("Result is "
& int'Image (V));
Put_Line ("Function was called "
& int'Image (func_cnt)
& " times");
end Show_C_Func;
애플리케이션을 실행해 보면 카운터 값이 my_func이 호출된 횟수임을 알 수 있어요. 서브프로그램과 같은 방식으로 External_Name 애스펙트를 사용해 Ada 애플리케이션에서 변수에 다른 이름을 줄 수 있어요.
C에서 Ada 변수 사용 (Using Ada variables in C)
C 애플리케이션에서 Ada 파일에 선언된 변수도 사용할 수 있어요. 서브프로그램에서 했던 것과 같은 방식으로 Export 애스펙트로 해요.
이전 예시를 재사용하고 이전 예시처럼 카운터를 추가할게요. 단 이번에는 카운터가 Ada 코드에서 증가되도록 해요:
with Interfaces.C; use Interfaces.C;
package C_API is
func_cnt : int := 0
with
Export => True,
Convention => C;
function My_Func (a : int) return int
with
Export => True,
Convention => C,
External_Name => "my_func";
end C_API;
그런 다음 변수는 My_Func에서 증가돼요:
package body C_API is
function My_Func (a : int) return int is
begin
func_cnt := func_cnt + 1;
return a * 2;
end My_Func;
end C_API;
C 애플리케이션에서는 변수를 선언하고 사용하기만 하면 돼요:
#include <stdio.h>
extern int my_func (int a);
extern int func_cnt;
int main (int argc, char **argv) {
int v;
v = my_func(1);
v = my_func(2);
v = my_func(3);
printf("Result is %d\n", v);
printf("Function was called %d times\n",
func_cnt);
return 0;
}
다시 한번 애플리케이션을 실행하면 카운터 값이 my_func이 호출된 횟수임을 볼 수 있어요.
바인딩 생성 (Generating bindings)
위 예시들에서 우리는 인터페이스하는 C 소스 코드에 대응하도록 Ada 코드에 애스펙트를 수동으로 추가했어요. 이를 바인딩(binding) 을 만든다고 해요. 이 과정을 Ada 스펙 덤프(Ada spec dump) 컴파일러 옵션 -fdump-ada-spec을 사용해 자동화할 수 있어요. 이전 예시를 다시 살펴보며 설명할게요.
다음이 우리의 C 헤더 파일이었어요:
extern int func_cnt;
int my_func (int a);
Ada 바인딩을 만들려면 컴파일러를 다음과 같이 호출해요:
gcc -c -fdump-ada-spec -C ./test.h
결과는 test_h.ads라는 Ada 스펙 파일이에요:
pragma Ada_2005;
pragma Style_Checks (Off);
with Interfaces.C; use Interfaces.C;
package test_h is
func_cnt : aliased int; -- ./test.h:3
pragma Import (C, func_cnt, "func_cnt");
function my_func (arg1 : int) return int; -- ./test.h:5
pragma Import (C, my_func, "my_func");
end test_h;
이제 우리는 Ada 애플리케이션에서 이 test_h 패키지를 참조하기만 하면 돼요:
with Interfaces.C; use Interfaces.C;
with Ada.Text_IO; use Ada.Text_IO;
with test_h; use test_h;
procedure Show_C_Func is
V : int;
begin
V := my_func (1);
V := my_func (2);
V := my_func (3);
Put_Line ("Result is "
& int'Image (V));
Put_Line ("Function was called "
& int'Image (func_cnt)
& " times");
end Show_C_Func;
만드는 바인딩의 상위 단위(parent unit) 이름을 fdump-ada-spec의 피연산자로 지정할 수 있어요:
gcc -c -fdump-ada-spec -fada-spec-parent=Ext_C_Code -C ./test.h
이것은 ext_c_code-test_h.ads 파일을 만들어요:
package Ext_C_Code.test_h is
-- 자동 생성된 바인딩...
end Ext_C_Code.test_h;
바인딩의 적응 (Adapting bindings)
컴파일러는 C 헤더 파일에 대한 바인딩을 만들 때 최선을 다해요. 하지만 때로는 번역에 대해 추측해야 하고, 생성된 바인딩이 항상 우리 기대에 맞지는 않아요. 예를 들어, 포인터를 인자로 갖는 함수에 대한 바인딩을 만들 때 이런 일이 발생할 수 있어요. 이 경우 컴파일러는 하나 이상의 포인터 타입으로 System.Address를 사용할 수 있어요. 이 접근 방식은 잘 작동하지만(뒤에서 보게 될 거예요), 보통 사람이 C 헤더 파일을 해석하는 방식은 아니에요. 다음 예시가 이 문제를 보여줘요.
이 C 헤더 파일로 시작할게요:
struct test;
struct test * test_create(void);
void test_destroy(struct test *t);
void test_reset(struct test *t);
void test_set_name(struct test *t,
char *name);
void test_set_address(struct test *t,
char *address);
void test_display(const struct test *t);
그리고 해당 C 구현:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "test.h"
struct test {
char name[80];
char address[120];
};
static size_t
strlcpy_stat(char *dst,
const char *src,
size_t dstsize)
{
size_t len = strlen(src);
if (dstsize) {
size_t bl = (len < dstsize-1 ?
len : dstsize-1);
((char*)memcpy(dst, src, bl))[bl] = 0;
}
return len;
}
struct test * test_create(void)
{
return malloc (sizeof (struct test));
}
void test_destroy(struct test *t)
{
if (t != NULL) {
free(t);
}
}
void test_reset(struct test *t)
{
t->name[0] = '\0';
t->address[0] = '\0';
}
void test_set_name(struct test *t,
char *name)
{
strlcpy_stat(t->name,
name,
sizeof(t->name));
}
void test_set_address(struct test *t,
char *address)
{
strlcpy_stat(t->address,
address,
sizeof(t->address));
}
void test_display(const struct test *t)
{
printf("Name: %s\n", t->name);
printf("Address: %s\n", t->address);
}
다음으로 바인딩을 만들게요:
gcc -c -fdump-ada-spec -C ./test.h
이것은 test_h.ads에 다음 스펙을 만들어요:
pragma Ada_2005;
pragma Style_Checks (Off);
with Interfaces.C; use Interfaces.C;
with System;
with Interfaces.C.Strings;
package test_h is
-- 빈 struct test는 건너뜀
function test_create return System.Address; -- ./test.h:5
pragma Import (C, test_create, "test_create");
procedure test_destroy (arg1 : System.Address); -- ./test.h:7
pragma Import (C, test_destroy, "test_destroy");
procedure test_reset (arg1 : System.Address); -- ./test.h:9
pragma Import (C, test_reset, "test_reset");
procedure test_set_name (arg1 : System.Address; arg2 : Interfaces.C.Strings.chars_ptr); -- ./test.h:11
pragma Import (C, test_set_name, "test_set_name");
procedure test_set_address (arg1 : System.Address; arg2 : Interfaces.C.Strings.chars_ptr); -- ./test.h:13
pragma Import (C, test_set_address, "test_set_address");
procedure test_display (arg1 : System.Address); -- ./test.h:15
pragma Import (C, test_display, "test_display");
end test_h;
보시다시피 바인딩 생성기는 struct test 선언을 완전히 무시하고 test 구조체에 대한 모든 참조를 주소(System.Address)로 바꿔요. 그럼에도 이 바인딩들은 Ada에서 테스트 애플리케이션을 만들기에 충분히 좋아요:
with Ada.Text_IO; use Ada.Text_IO;
with Interfaces.C;
use Interfaces.C;
with Interfaces.C.Strings;
use Interfaces.C.Strings;
with test_h; use test_h;
with System;
procedure Show_Automatic_C_Struct_Bindings is
Name : constant chars_ptr :=
New_String ("John Doe");
Address : constant chars_ptr :=
New_String ("Small Town");
T : System.Address := test_create;
begin
test_reset (T);
test_set_name (T, Name);
test_set_address (T, Address);
test_display (T);
test_destroy (T);
end Show_Automatic_C_Struct_Bindings;
자동 생성된 바인딩으로도 C 코드를 Ada와 성공적으로 바인딩할 수 있지만, 이상적이지는 않아요. 대신 우리가 (사람으로서) C 헤더 파일을 해석하는 방식과 일치하는 Ada 바인딩을 선호해요. 이를 위해서는 헤더 파일을 수동으로 분석해야 해요. 좋은 소식은 자동 생성된 바인딩을 출발점으로 사용해 필요에 맞게 적응시킬 수 있다는 거예요. 예를 들어 다음과 같은 것들을 할 수 있어요:
System.Address에 기반한Test타입을 정의하고 모든 관련 함수에서 사용하기Test타입에 대한 모든 연산에서test_접두사를 제거하기
이것이 결과 스펙이에요:
with System;
with Interfaces.C; use Interfaces.C;
with Interfaces.C.Strings;
package adapted_test_h is
type Test is new System.Address;
function Create return Test;
pragma Import (C, Create, "test_create");
procedure Destroy (T : Test);
pragma Import (C, Destroy, "test_destroy");
procedure Reset (T : Test);
pragma Import (C, Reset, "test_reset");
procedure Set_Name (T : Test;
Name : Interfaces.C.Strings.chars_ptr); -- ./test.h:11
pragma Import (C, Set_Name, "test_set_name");
procedure Set_Address (T : Test;
Address : Interfaces.C.Strings.chars_ptr);
pragma Import (C, Set_Address, "test_set_address");
procedure Display (T : Test); -- ./test.h:15
pragma Import (C, Display, "test_display");
end adapted_test_h;
그리고 이것이 해당 Ada 본문이에요:
with Interfaces.C;
use Interfaces.C;
with Interfaces.C.Strings;
use Interfaces.C.Strings;
with adapted_test_h; use adapted_test_h;
with System;
procedure Show_Adapted_C_Struct_Bindings is
Name : constant chars_ptr :=
New_String ("John Doe");
Address : constant chars_ptr :=
New_String ("Small Town");
T : Test := Create;
begin
Reset (T);
Set_Name (T, Name);
Set_Address (T, Address);
Display (T);
Destroy (T);
end Show_Adapted_C_Struct_Bindings;
이제 Test 타입과 그 연산들을 깔끔하고 읽기 좋게 사용할 수 있어요.