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 코드의 전역 변수를 사용하려면 서브프로그램과 같은 방법을 사용해요: 가져오려는 각 변수에 ImportConvention 애스펙트를 지정해요.

이전 절의 예시를 재사용할게요. 함수(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 타입과 그 연산들을 깔끔하고 읽기 좋게 사용할 수 있어요.

더 알아보기 (Learn more)