C와의 인터페이싱

C와의 인터페이싱 (Interfacing to C)

D는 대상 시스템의 C 컴파일러와 편안하게 어울리도록 설계돼 있어요. 래퍼(wrapper) 함수를 만들지 않고도 C 함수를 직접 호출할 수 있어요.

이게 가능한 이유는 D가 C 컴파일러의 데이터 타입과 레이아웃, 그리고 함수 호출·복귀 시퀀스(function call/return sequence)를 그대로 맞춰 주기 때문이에요.

ImportC 컴파일러 확장은 .c 파일을 직접 import하거나 컴파일할 수 있게 해 줘요.

인기 있는 C 라이브러리용 바인딩(binding)은 표준 라이브러리와 패키지 저장소(package repository)에서 찾을 수 있어요.

이 페이지의 나머지 부분은 C와 인터페이싱하는 수작업(low-level) 측면을 다뤄요.

출처: https://dlang.org/spec/interfaceToC.html

본문

목차

  • C 함수 호출 (Calling C Functions)
  • 저장소 할당 (Storage Allocation)
  • 데이터 타입 호환성 (Data Type Compatibility)
  • C 함수에 D 배열 인자 넘기기 (Passing D Array Arguments to C Functions)
  • printf() 호출 (Calling printf())
  • 구조체와 공용체 (Structs and Unions)
  • 콜백 (Callbacks)
  • 기존 C 라이브러리 사용하기 (Using Existing C Libraries)
  • C 전역 변수 접근 (Accessing C Globals)

Calling C Functions — C 함수 호출

C 함수는 D에서 직접 호출할 수 있어요. 래퍼 함수도, 인자 스위즐링(argument swizzling)도 필요 없고, C 함수를 별도의 DLL에 넣을 필요도 없어요.

C 함수는 반드시 선언하고 호출 규약(calling convention)을 지정해야 해요. 대부분 "C" 호출 규약을 쓰죠. 예를 들면 이렇게요:

extern (C) int strcmp(const char* string1, const char* string2);

그런 다음 D 코드 안에서 당연하게 이렇게 호출하면 돼요:

import std.string;
int myDfunction(char[] s)
{
    return strcmp(std.string.toStringz(s), "foo");
}

여기서 몇 가지 짚고 갈 게 있어요.

  • D는 C 함수 이름이 어떻게 "맹글링(mangling)"되는지, 그리고 올바른 C 함수 호출·복귀 시퀀스를 이해해요.
  • C 함수는 이름이 같은 다른 C 함수로 오버로드(overload)할 수 없어요.
  • D에는 __cdecl, __far, __stdcall, __declspec 같은 C 확장 타입 한정자(C extended type modifier)가 없어요. 이런 것들은 extern (C) 같은 링키지 속성(linkage attribute)이 처리해 줘요.
  • D에는 volatile 타입 한정자가 없어요. volatile을 쓰는 C 함수를 선언할 땐 그 키워드를 선언에서 빼 버리면 돼요.
  • D에서 문자열은 0으로 끝나지 않아요. 자세한 내용은 "Data Type Compatibility" 섹션을 참고하세요. 다만 D의 문자열 리터럴(string literal)은 0으로 끝나요.

C 코드도 그 반대로 D 함수를 호출할 수 있어요. 단, D 함수가 C 컴파일러와 호환되는 속성, 대부분 extern (C)를 쓰고 있어야 해요:

// myfunc() can be called from any C function
extern (C)
{
    void myfunc(int a, int b)
    {
        ...
    }
}

Storage Allocation — 저장소 할당

C 코드는 malloc()과 free()를 호출하며 메모리를 명시적으로 관리해요. 반면 D는 D 가비지 컬렉터(GC)로 메모리를 할당하므로 명시적인 free가 필요 없어요.

물론 D도 core.stdc.stdlib.malloc()과 core.stdc.stdlib.free()로 메모리를 명시적으로 할당할 수 있어요. 이런 것들은 malloc으로 할당된 버퍼를 기대하는 C 함수를 연결할 때 유용해요.

D 가비지 컬렉터가 할당한 메모리의 포인터를 C 함수에 넘긴다면, C 함수가 그 메모리를 다 쓰기 전에 GC가 수집해 버리지 않도록 하는 게 아주 중요해요. 이건 여러 방법으로 보장할 수 있어요.

  • core.stdc.stdlib.malloc()으로 데이터를 복사하고 그 복사본을 넘긴다.
  • 스택(stack)에 포인터를 남겨둔다(매개변수나 자동 변수로). GC는 스택을 검사하기 때문이에요.
  • 정적 데이터 세그먼트(static data segment)에 포인터를 남겨둔다. GC는 정적 데이터 세그먼트도 검사하니까요.
  • std.gc.addRoot() 또는 std.gc.addRange() 호출로 포인터를 GC에 등록한다.

할당된 메모리 블록의 내부 포인터(interior pointer)만 있어도 GC가 그 객체가 사용 중이라는 걸 알 수 있어요. 즉, 할당된 메모리의 시작점에 대한 포인터를 유지할 필요는 없어요.

가비지 컬렉터는 D Thread 인터페이스가 만든 스레드가 아닌 스레드의 스택은 검사하지 않아요. 다른 DLL들의 데이터 세그먼트도 마찬가지로 검사하지 않아요.

Data Type Compatibility — 데이터 타입 호환성

이 동등성(equivalent)은 대부분의 C 컴파일러에서 성립해요. C 표준은 타입의 크기를 고정하지 않으므로 조금 주의가 필요해요.

Passing D Array Arguments to C Functions — C 함수에 D 배열 인자 넘기기

C에서 배열은 함수 원형(prototype)이 배열이라고 적혀 있어도 포인터로 함수에 전달돼요. 반면 D에서 정적 배열(static array)은 참조가 아니라 값으로 전달돼요. 그래서 C가 기대하는 것에 맞추려면 함수 원형을 조정해야 해요.

예를 들면 이렇게요:

void foo(int a[3]) { ... } // C code
extern (C)
{
    void foo(ref int[3] a); // D prototype
}

Calling printf() — printf() 호출

printf는 D 코드에서 바로 호출할 수 있어요:

import core.stdc.stdio;

int main()
{
    printf("hello world\n");
    return 0;
}

값을 출력하는 것도 C에서처럼 동작해요:

int apples;
printf("there are %d apples\n", apples);

포맷 지정자(format specifier)를 D 타입에 정확히 맞추는 게 필요해요. D 컴파일러는 printf 포맷을 인식해서 전달된 인자와의 불일치를 진단해 줘요. D가 쓰는 포맷의 사양은 C99 사양 7.19.6.1이에요.

인자와 포맷 지정자가 맞는지 판단할 땐 관대하게(넓게) 해석해요. 예를 들어 부호 없는 타입을 부호 있는 포맷 지정자로 출력할 수 있어요. 진단되는 비호환성(incompatibility)은 다음과 같아요.

  • 인자 정렬이 어긋날 수 있는 크기 불일치
  • 포인터가 아닌 인자를 역참조(dereference)하는 경우
  • 인자 수가 부족한 경우
  • struct, 배열, 슬라이스 인자는 허용되지 않아요
  • s 지정자에 포인터가 아닌 인자를 넘기는 경우
  • 비표준(Non-Standard) 포맷
  • C99 기준 정의되지 않은 동작(undefined behavior)

Strings — 문자열

문자열을 직접 출력할 수는 없어요. 하지만 %.*s를 쓸 수 있죠:

string s = "betty";
printf("hello %.*s\n", cast(int) s.length, s.ptr);

int로의 캐스트(cast)가 필요해요.

size_t and ptrdiff_t — size_t와 ptrdiff_t

이들은 각각 zu와 td 포맷 지정자를 써요:

import core.stdc.stdio : printf;
int* p = new int, q = new int;
printf("size of an int is %zu, pointer difference is %td\n", int.sizeof, p - q);

Non-Standard Format Specifiers — 비표준 포맷 지정자

비표준 포맷 지정자는 컴파일러가 거부해요. 다만 그 검사는 문자열 리터럴로 된 포맷에만 이루어지므로, 비표준 것도 이렇게 쓰면 돼요:

const char* format = "value: %K\n";
printf(format, value);

Modern Formatted Writing — 현대적인 포맷 출력

포맷 출력을 위한 개선된 D 함수로 std.stdio.writef()가 있어요.

Structs and Unions — 구조체와 공용체

D의 구조체(struct)와 공용체(union)는 C의 것과 유사해요.

C 코드는 명령줄 스위치나 구현별(implementation specific) #pragma로 구조체 멤버의 정렬(alignment)과 패킹(packing)을 조정하는 경우가 많아요. D는 C 컴파일러의 규칙에 대응하는 명시적 정렬 속성을 지원해요. C 코드가 쓰는 정렬을 확인해서 D 구조체 선언에도 명시적으로 그 정렬을 설정해 주세요.

D는 비트 필드(bit field)를 지원하지 않아요. 필요하면 시프트(shift)와 마스크(mask) 연산으로 흉내 내거나, std.bitmanip.bitfields 라이브러리 타입을 쓸 수 있어요. htod는 비트 필드를 올바른 우시프트와 마스크를 수행하는 인라인 함수로 변환해 줘요.

D는 익명(anonymous) 구조체 타입의 변수 선언을 지원하지 않아요. 그런 경우에는 D에서 이름 있는 구조체를 정의하고 private로 만들면 돼요:

union Info  // C code
{
    struct
    {
        char *name;
    } file;
};
union Info  // D code
{
    private struct File
    {
        char* name;
    }
    File file;
}

Callbacks — 콜백

D는 C 콜백(함수 포인터)을 쉽게 호출할 수 있어요. 그리고 그 콜백이 extern(C) 함수이거나 양쪽이 합의한 다른 링키지(예: extern(Windows))라면, C가 D 코드가 제공한 콜백을 호출할 수도 있어요.

여기 C 코드가 D 코드에 콜백을 제공하는 예시가 있어요:

void someFunc(void *arg) { printf("Called someFunc!\n"); }  // C code
typedef void (*Callback)(void *);
extern "C" Callback getCallback(void)
{
    return someFunc;
}
extern(C) alias Callback = int function(int, int);  // D code
extern(C) Callback getCallback();
void main()
{
    Callback cb = getCallback();
    cb();  // invokes the callback
}

그리고 D 코드가 C 코드에 콜백을 제공하는 예시예요:

extern "C" void printer(int (*callback)(int, int))  // C code
{
    printf("calling callback with 2 and 4 returns: %d\n", callback(2, 4));
}
extern(C) alias Callback = int function(int, int);  // D code
extern(C) void printer(Callback callback);
extern(C) int sum(int x, int y) { return x + y; }
void main()
{
    printer(&sum);
}

콜백에 대한 더 자세한 내용은 클로저(closures) 섹션을 읽어 보세요.

Using Existing C Libraries — 기존 C 라이브러리 사용하기

D가 C 코드를 직접 호출할 수 있으니, 어떤 C 라이브러리 함수든 호출할 수 있어요. 그 덕분에 D는 기성 C 라이브러리들의 스모가스보드(smorgasbord, 풍성한 선택지)에 접근할 수 있죠. 다만 그러려면 C 라이브러리의 C .h 헤더 파일을 D로 번역한 D 인터페이스(.di) 파일을 작성해야 해요.

인기 있는 C 라이브러리의 경우, 해당 D 인터페이스 파일을 찾을 첫 번째 장소는 Deimos 프로젝트예요. 거기에 아직 없다면 직접 작성해서 Deimos 프로젝트에 기여해 주세요.

Accessing C Globals — C 전역 변수 접근

C 전역 변수(global)는 D에서 직접 접근할 수 있어요. C 전역 변수는 C 네이밍 규약을 따르므로 extern (C) 블록 안에 있어야 해요. extern 저장 클래스(storage class)는 그 전역 변수가 D 코드가 아니라 C 코드에 할당돼 있음을 나타내는 데 써요. C 전역 변수는 기본적으로 스레드 로컬(thread local)이 아니라 전역(global) 저장소에 있어요. D에서 전역 저장소를 참조하려면 __gshared 저장 클래스를 쓰세요.

extern (C) extern __gshared int x;

더 알아보기 (Learn more)