ImportC

ImportC

D 언어 구현 안에 내장되어 있는 C 컴파일러인 ImportC에 대해, 작동 방식부터 지원 범위까지 차근차근 살펴볼게요. ImportC는 C 파일을 그대로 가져와 D 코드와 함께 링크해서 실행 파일을 만들 수 있게 해 주는 녀석이에요.

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

본문

ImportC는 D 구현에 내장된 C 컴파일러예요. 덕분에 C 파일에 선언된 내용에 대응하는 D 파일을 손으로 준비할 필요 없이, C 파일을 직접 가져올 수 있어요. C 파일을 모듈로 직접 컴파일해서 D 코드와 링크해 실행 파일을 만들 수 있고요, 100% C 프로그램을 컴파일하고 링크하는 C 컴파일러로도 쓸 수 있어요.

참고로 ImportC와 BetterC는 아주 달라요. ImportC는 실제 C 컴파일러지만, BetterC는 C 표준 라이브러리의 존재에만 기대는 D의 부분집합이에요. BetterC 코드는 ImportC 코드와도 링크할 수 있답니다.

빠른 예제 (Quick Examples)

hello.c 파일에 있는 C 코드:

#include <stdio.h>
int main()
{
    printf("hello world\n");
    return 0;
}

컴파일하고 실행:

dmd hello.c
./hello
hello world

functions.c 파일에 있는 C 함수:

int square(int i)
{
    return i * i;
}

demo.d 파일에 있는 D 프로그램:

import std.stdio;
import functions;
void main()
{
    int i = 7;
    writefln("The square of %s is %s", i, square(i));
}

컴파일하고 실행:

dmd demo.d functions.c
./demo
The square of 7 is 49

ImportC 방언 (ImportC Dialect)

C에는 버전이 아주 많아요. ImportC는 ISO/IEC 9899:2011, 줄여서 C11이라고 부르는 규격의 구현이에요. C11 규격을 언급할 때는 "C11 다음에 문단 번호"로 표현할게요. 이전 버전인 C99, C89, K+R C는 지원하지 않아요.

여기에 더해, D 구현이 가진 능력 몇 가지를 활용할 수 있도록 추가 조정이 이루어져 있어요.

ImportC 실행하기 (Invoking ImportC)

ImportC 컴파일러는 두 가지 방법으로 실행할 수 있어요.

  • 명령 줄에서 직접
  • C 파일을 import해서 간접적으로

명령 줄의 ImportC 파일 (ImportC Files on the Command Line)

ImportC 파일은 .i, .h, .c 확장자 중 하나를 가져요.

dmd hello.c

이렇게 하면 ImportC로 hello.c를 컴파일하고 링크해서 실행 파일 hello(Windows에서는 hello.exe)를 만들어요.

D 코드에서 C 파일 import 하기 (Importing C Files from D Code)

D의 ImportDeclaration을 사용하면 돼요.

import hello;

hello가 D 파일이 아니고 확장자가 .i, .h, .c라면, ImportC가 hello를 컴파일해 줘요.

패키지를 통해 import되어 컴파일되는 C 파일은 그 패키지를 포함하는 모듈 선언을 가져야 해요. 이때 __IMPORTC__ 매크로로 가드한 __module 키워드를 써서 ImportC 모듈로 레이블을 붙여 주면 돼요.

project/net/hi.c에 있는 C 파일:

#if __IMPORTC__
__module net.hi
#endif

int sqrt(int x) { return x * x; }

project/hello.d에 있는 D 파일:

import net.hi; // C file you need in your D source

void main()
{
    assert(sqrt(3) == 9);
}
user@ -- project % dmd hello.d net/hi.c

전처리기 (Preprocessor)

ImportC에는 전처리기가 없어요. C 파일을 C 전처리기에 먼저 통과시킨 뒤 컴파일하도록 설계되어 있어요. ImportC는 연결된 C 컴파일러(Associated C Compiler)에 딸린 C 전처리기를 자동으로 실행할 수도 있고, 전처리기를 수동으로 실행할 수도 있어요.

전처리기 자동 실행 (Running the Preprocessor Automatically)

C 파일에 .c 확장자가 있으면 ImportC가 전처리기를 자동으로 실행해 줘요.

  • Windows용으로 -m32omf 스위치로 컴파일할 때는 sppn.exe를 전처리기로 써요.
  • Windows용으로 -m32mscoff 또는 -m64 스위치로 컴파일할 때는 cl.exe /P /Zc:preprocessor를 전처리기로 써요.
  • OSX에서 컴파일할 때는 clang -E 전처리기를 써요.
  • 그 외에는 cpp 전처리기를 써요.

-v 스위치를 쓰면 전처리기를 호출하는 명령을 볼 수 있어요.

-Ppreprocessorflag 스위치는 preprocessorflag를 전처리기에 그대로 전달해 줘요.

importc.h

druntime 파일 src/importc.h가 자동으로 맨 먼저 #include돼요. importc.h는 ImportC가 지원하지 않는 여러 C 컴파일러 확장을 소스 코드에서 보정해 주는 역할을 해요.

ImportC include 디렉터리 (ImportC include Directory)

importc.h 파일이 있는 디렉터리 안에는 include라는 이름의 디렉터리가 있고, 그 경로가 C 전처리기의 include 경로에 추가로 공급돼요.

ImportC include 디렉터리에는 ImportC가 제공하는 C 헤더 파일이 들어 있어요. 제공되는 헤더는 다음과 같아요.

  • importc_msvc_builtins.h: MSVC의 컴파일러 내장 함수(intrinsics)를 지원해 줘요.
no-builtin-macro-redefined

Posix 시스템에서 ImportC는 gcc와 clang이 쓰는 C 전처리기에 -Wno-builtin-macro-redefined 스위치를 전달해요. 이 스위치는 2008년 이전에 만들어진 gcc 전처리기에는 없어요. 그럴 땐 전처리기를 수동으로 실행하는 게 해결책이에요.

전처리기 수동 실행 (Running the Preprocessor Manually)

C 파일에 .i 확장자가 있으면 이미 전처리가 끝난 파일로 간주돼요. 전처리는 수동으로도 실행할 수 있어요.

Digital Mars C 전처리기 sppn.exe

sppn.exe는 Win32에서 동작하며 다음과 같이 실행해요.

sppn file.c

전처리된 결과는 file.i로 쓰여요.

Gnu C 전처리기

Gnu C 전처리기는 다음과 같이 실행할 수 있어요.

gcc -E file.c > file.i
Clang C 전처리기

Clang 전처리기는 다음과 같이 실행할 수 있어요.

clang -E file.c -o file.i
Microsoft VC 전처리기

VC 전처리기는 다음과 같이 실행할 수 있어요.

cl /P /Zc:preprocessor file.c -Fifile.i

전처리된 결과는 file.i로 쓰여요.

dmpp C 전처리기

dmpp C 전처리기는 다음과 같이 실행할 수 있어요.

dmpp file.c

전처리된 결과는 file.i로 쓰여요.

전처리기 매크로 (Preprocessor Macros)

ImportC는 자동으로 전처리기를 실행할 때 전처리기 실행 결과로 나온 #define 매크로를 전부 수집해요. 그중 일부는 선언으로 해석해서 D 코드에서 쓸 수 있게 만들어 줘요. D 선언으로 해석할 수 있는 매크로의 종류는 더 늘어날 수 있겠지만, C 매크로의 메타프로그래밍 용법 전체를 다 포괄하지는 않을 거예요.

매니페스트 상수 (Manifest Constants)

다음처럼 매니페스트 상수처럼 생긴 매크로는:

#define COLOR 0x123456
#define HELLO "hello"

다음 형태의 D 매니페스트 상수 선언으로 해석돼요.

enum COLOR = 0x123456;
enum HELLO = "hello";
함수형 매크로 (Function-Like Macros)

많은 매크로는 함수처럼 생겼고, 템플릿 함수처럼 다룰 수 있어요.

#define ABC a + b
#define DEF(a) (a + x)
auto ABC() { return a + b; }
auto DEF(T)(T a) { return a + x; }

하지만 어떤 매크로 형태는 같은 결과를 내지 못해요.

#define ADD(a, b) a + b
int x = ADD(1, 2) * 4; // sets x to 9
auto ADD(U, V)(U a, V b) { return a + b; }
int x = ADD(1, 2) * 4; // sets x to 12
#define ADD(a, b) ((a) + (b))

문제가 되는 또 다른 영역은 인자 쪽의 부작용(side effect)이에요.

#define DOUBLE(x) ((x) + (x))
int i = 0;
DOUBLE(i++);
assert(i == 2);  // D result will be 1, C result will be 2

그리고 인자를 참조로 다루는 경우도 있어요.

#define INC(x) (++x)
int i = 0;
INC(i);
assert(i == 1); // D result will be 0, C result will be 1

사전 정의된 매크로 (Predefined Macros)

ImportC는 어떤 매크로도 미리 정의하지 않아요.

ImportC 컴파일을 다른 C 컴파일러와 구별하려면 다음을 사용해요.

#if __IMPORTC__

__IMPORTC__src/importc.h에 정의되어 있고, 전처리기가 실행될 때 자동으로 포함돼요. importc.h에는 다양한 C 소스 코드의 잔재들을 ImportC에 맞게 적응시키는 데 쓰는 매크로 정의가 많이 들어 있어요.

전처리기 지시어 (Preprocessor Directives)

ImportC는 다음 전처리기 지시어들을 지원해요.

Line control

C11 6.10.4

Linemarker

linemarker 지시어는 보통 C 전처리기의 출력에 내장돼 있어요.

프라그마 (Pragmas)

다음 프라그마들을 지원해요.

  • #pragma pack ( )
  • #pragma pack ( show )
  • #pragma pack ( push )
  • #pragma pack ( push , identifier )
  • #pragma pack ( push , integer )
  • #pragma pack ( push , identifier , integer )
  • #pragma pack ( pop )
  • #pragma pack ( pop PopList )
#pragma attribute
#pragma attribute(push, [storage classes...])
#pragma attribute(push, nogc, nothrow)
#include <somelibrary.h>
#pragma attribute(pop)
Construct Consideration #pragmas

Construct Consideration 프라그마는 ImportC 전용 프라그마로, 특정 식별자를 가진 종류의 선언과 정의를 ImportC가 고려할지(consider) 무시할지(ignore) 바꾸는 데 써요.

Construct Consideration 프라그마는 한 가지 형태를 가져요.

  • #pragma <category>(<operation>, <identifier>,...)

즉, 프라그마의 이름이 C 구문(구조체)의 한 범주이고, 그다음에 연산(operation), 그다음에 쉼표로 구분된 식별자 목록이 와요. 식별자 목록 끝에는 쉼표를 써도 돼요.

Construct Consideration 프라그마의 범주(category)는 다음과 같아요.

  • function_decl: 주어진 이름의 함수 선언(function declaration, 예: void foo(int);)을 ImportC가 고려할지 무시할지를 제어해요.
  • function_def: 주어진 이름의 함수 정의(function definition, 예: void foo(int x) {})를 ImportC가 고려할지 무시할지를 제어해요.

Construct Consideration 프라그마가 인식하는 연산(operation)은 다음과 같아요.

  • consider: 이름 붙은 구문을 평소처럼 고려하도록 해요. 기본 동작이에요.
  • ignore: 이름 붙은 구문을 무시하도록 해요. 여전히 파싱은 되지만 무기력하게(inert) 처리되어 효과가 없어요.

Construct Consideration 프라그마에 지정된 연산은 프라그마에 주어진 식별자들 중 하나와 같은 이름을 가진 구문에만 적용돼요.

예를 들면:

  • #pragma function_decl(ignore, foo, bar): foobar라는 이름의 C 함수 선언을 무시해요.
  • #pragma function_decl(consider, foo, bar): foobar라는 이름의 C 함수 선언을 고려(무시를 중단)해요.
  • #pragma function_def(ignore, foo, bar): foobar라는 이름의 C 함수 정의를 무시해요.
  • #pragma function_def(consider, foo, bar): foobar라는 이름의 C 함수 정의를 고려(무시를 중단)해요.
// We start ignoring function-declarations and function-definitions of foo and bar.
#pragma function_decl(ignore, foo, bar)
#pragma function_def(ignore, foo, bar)
// This declaration of foo is ignored.
void foo(int);
// As is this definition of foo.
void foo(int x)
{}

// We stop ignoring function-definitions of bar.
#pragma function_def(consider, bar)
// This declaration of bar is ignored.
void bar(int);
// This definition of bar is not ignored.
float bar(float x, float y)
{
    return x * y;
}

src/__importc_builtins.di

컴파일러가 전처리를 마치면 가장 먼저 하는 일은 src/__importc_builtins.di를 import하는 거예요. 이 파일은 다른 C 컴파일러들이 제공하는 다양한 내장 기능(builtins)을 지원해 줘요. __importc_builtins.di는 D 파일이에요.

src/__builtins_msvc.d

MSVC의 컴파일러 내장 함수 지원은 D로 구현된 __builtins_msvc 모듈을 통해 제공돼요.

__importc_builtins.di와 달리 __builtins_msvc는 기본으로 import되지 않아요. 대신 importc_msvc_builtins.h 헤더가 __builtins_msvc 모듈을 import해요.

구현 (Implementation)

ImportC의 구현 정의 특성(implementation defined characteristics)은 다음과 같아요.

열거형 (Enums)

enumeration-constant는 항상 정수로 타입이 정해져요.

enumeration-constant의 값을 정의하는 표현식(expression)은 반드시 정수여야 해요. 열거형에 기반 int가 지정되어 있으면, 정의 표현식은 int에 들어맞는 값으로 평가되어야 해요. 기반 int가 지정되지 않았다면, 컴파일러가 표현식의 크기를 수용할 수 있을 만큼 큰 정수 타입을 골라요.

enum E { a = -10, b = 0x81231234 }; // ok
enum F { c =  0x812312345678 }; // ok, c expands to 8 byte to accomodate expression
enum G : int { c = 0x80000000 }; // error, doesn't fit in int
enum G { d = 1.0 };             // error, not integral type

비트 필드 (Bit Fields)

C11 비트 필드에는 구현 정의 측면이 아주 많아요. ImportC의 동작은 대상 플랫폼의 연결된 C 컴파일러 동작에 맞춰 조정돼요.

wchar_t

Windows를 대상으로 할 때 wchar_t__importc_wchar의 typedef예요. Windows가 아닌 플랫폼을 대상으로 할 때 wchar_t는 특별한 대우를 받지 않고, 평소처럼 <stddef.h> 헤더가 정의해 줘요.

암묵적 함수 선언 (Implicit Function Declarations)

암묵적 함수 선언:

int main()
{
    func();  // implicit declaration of func()
}

이런 건 K+R C와 C89에서는 허용됐지만 C99와 C11에서 무효화됐어요. 많은 C 컴파일러가 여전히 지원하지만 ImportC는 지원하지 않아요.

#pragma STDC FENV_ACCESS

이것은 C11 7.6.1에 설명되어 있어요.

#pragma STDC FENV_ACCESS on-off-switch

on-off-switch:
    ON
    OFF
    DEFAULT

완전히 무시돼요.

제한 (Limitations)

예외 처리 (Exception Handling)

ImportC는 절대 예외를 던지지 않는다고 가정해요. setjmplongjmp는 지원하지 않아요.

const

C11은 const가 지역적으로만 적용된다고 규정해요. ImportC에서 const는 전이적으로(transitively) 적용돼요. 즉:

int *const p;

이 코드가 C11에서는 pint를 가리키는 const 포인터라는 뜻이지만, ImportC에서는 p가 const int를 가리키는 const 포인터라는 뜻이 돼요.

volatile

volatile 타입 한정자(C11 6.7.3)는 무시돼요. 어차피 volatile을 써서 공유 메모리 접근을 구현하는 건 잘 되지 않을 가능성이 높아요. 그건 _Atomic이 담당하는 일이에요. volatile을 장치 레지스터로 쓰고 싶다면, 별도로 컴파일되는 함수를 호출해서 처리하거나 인라인 어셈블러를 쓰세요.

restrict

restrict 타입 한정자(C11 6.7.3)는 무시돼요.

_Atomic

_Atomic 타입 한정자(C11 6.7.3)는 무시돼요. 원자적 연산이 필요하면 그 용도로 외부에서 컴파일된 함수를 쓰거나 인라인 어셈블러를 사용하세요.

호환 타입 (Compatible Types)

ImportC에서 호환 타입(Compatible Types, C11 6.7.2)은 동일한 타입이에요.

임피던스 불일치 (Impedance Mismatch)

C와 D가 "그냥 잘 돌아가게" 맞추려고 모든 노력을 기울이고 있지만, 두 언어는 이따금 드러나는 근본적인 차이점이 있어요.

키워드 불일치 (Keyword Mismatch)

D와 C는 대부분 같은 키워드를 쓰지만, C에는 D에 없는 키워드가 있고 그 반대도 있어요. 이는 C 코드 컴파일에는 영향을 주지 않지만, D에서 C 변수와 타입에 접근할 때 어려움을 줄 수 있어요. 예를 들어 D의 version 키워드는 C에서 구조체 멤버로 흔하지 않게 쓰이곤 해요.

defs.c 파일에 있는 C 코드:

struct S { int version; };

D에서 접근:

import defs;
int tech(S* s) {
    return s.version; // fails because version is a D keyword
}

해결 방법이 있어요:

import defs;
int tech(S* s) {
    return __traits(getMember, *s, "version");
}

같지만 다른 타입 (Same only Different Types)

어떤 플랫폼에서는 C의 longunsigned longintunsigned int와 각각 같은 크기예요. 다른 플랫폼에서는 C의 longunsigned longlong longunsigned long long과 같은 크기이고요. long doublelong double _Complexdoubledouble _Complex와 같은 크기일 수 있어요. ImportC에서는 같은 크기·같은 부호를 가진 이 타입들을 같은 타입으로 취급해요.

_Generic

제네릭 선택 표현식(Generic selection expressions, C11 6.5.1.1)은 ImportC와 달라요. 위 '같지만 다른 타입'의 타입들은 generic-association의 type-name 부분에서 구별이 안 돼요. ImportC는 C11 6.5.1.1-2대로 중복 타입에 대해 오류를 내는 대신, generic-assoc-list에서 첫 번째 호환 type-name을 선택해요.

확장 (Extensions)

asm 문 (Asm statement)

D 언어에서 asm은 표준 키워드이고, 그 구문은 ImportC와 공유돼요. C 언어에서는 asm이 확장이므로(J.5.10) __asm__을 쓰는 걸 권장해요. asm의 모든 대체 키워드는 전처리 단계에서 druntime 파일 src/importc.h가 변환해 줘요.

asm 키워드는 어셈블러 명령을 내장(embed)하는 데 쓸 수 있고, 그 문법은 구현 정의예요. Digital Mars D 컴파일러는 D x86 인라인 어셈블러 문서에 설명된 방언의 인라인 어셈블리만 지원해요.

함수나 변수 선언에서 asm(mangleName)은 심볼의 맹글 이름(mangle name)을 지정하는 데 쓸 수 있어요. 사용법은 pragma mangle과 비슷해요.

char **myenviron asm("environ") = 0;

int myprintf(char *, ...) asm("printf");

변수에 레지스터를 연관시키는 asm 용법은 무시돼요.

전방 참조 (Forward References)

스코프 안의 어떤 선언이든 접근할 수 있어요. 단순히 참조보다 어휘적으로 앞서 있는 선언만 접근할 수 있는 게 아니에요.

Ta *p;  // Ta is forward referenced
struct Sa { int x; };
typedef struct Sa Ta; // Ta is defined
struct S s;
int* p = &s.t.x;  // struct S definition is forward referenced
struct S { int a; struct T t; }; // T still forward referenced
struct T { int b; int x; }; // definition of struct T

C++ 스타일 태그 심볼 (C++ Style Tag Symbols)

C++에서는 같은 스코프에 같은 이름의 다른 선언이 없기만 하면, struct, union, enum 태그 심볼을 그 키워드를 앞에 붙이지 않고 접근할 수 있어요. ImportC도 똑같이 동작해요.

예를 들어 다음 코드는 C++과 ImportC 둘 다에서 받아들여져요.

struct s { int a; };

void g(int s)
{
    struct s* p = (struct s*)malloc(sizeof(struct s)); // unambiguous
    p->a = s;
}

반면 이건 C++과 ImportC 둘 다 같은 이유로 거부돼요.

struct s { int a; };

void g(int s)
{
    s* p = (s*)malloc(sizeof(s)); // error: parser matches s to int s
    p->a = s;
}

컴파일 타임 함수 실행 (Compile Time Function Execution)

상수 표현식 평가는 D의 CTFE가 할 수 있는 것과 같은 방식으로 함수를 실행하는 것을 포함해요. constant-expression은 컴파일 타임 평가를 호출해요.

예시:

_Static_assert("\x1"[0] == 1, "indexing should be 1");

int mint1() { return -1; }
_Static_assert(mint1() == -1, "call should be -1");

const int a = 7;
int b = a; // sets b to 7

함수 인라이닝 (Function Inlining)

함수 본문이 존재하는 함수는 ImportC가 인라인할 수 있어요. 그것을 호출하는 D 코드도 인라인할 수 있고요.

열거형 기반 타입 (Enum Base Types)

열거형에는 선택적인 EnumBaseType이 추가로 제공돼요.

EnumDeclaration:
    enum Identifier : EnumBaseType EnumBody

EnumBaseType:
    Type

이것이 제공되면 열거형 멤버들이 EnumBaseType으로 암묵적으로 캐스팅돼요.

enum S : byte { A };
_Static_assert(sizeof(A) == 1, "A should be size 1");

D 문자 타입 (D Character Types)

ImportC에서는 __importc_char, __importc_wchar, __importc_dchar 앨리어스로 D의 문자 타입을 쓸 수 있어요.

register 저장 클래스 (Register Storage Class)

register 저장 클래스를 가진 객체는 auto 선언으로 취급돼요.

register 저장 클래스를 가진 객체는 주소를 취할 수 있어요. C11 6.3.2.1-2

배열은 register 저장 클래스를 가질 수 있고, 컴파일러가 레지스터에 할당(enregister)할 수 있어요. C11 6.3.2.1-3

typeof 연산자 (typeof Operator)

typeof 연산자를 타입 지정자로 쓸 수 있어요.

type-specifier:
    typeof-specifier

typeof-specifier:
    typeof ( expression )
    typeof ( type-name )

import 선언 (Import Declarations)

모듈은 CImportDeclaration으로 import할 수 있어요.

CImportDeclaration:
    __import ImportList ;

Import는 그 선언들을 나타내는 .h 파일을 만들 필요 없이 ImportC 코드가 D 선언과 함수에 직접 접근할 수 있게 해 줘요. .h 파일을 D 선언과 계속 동기화해야 하는 지루함과 취약함이 사라져요. D 함수는 인라인할 수 있게 되고요.

Import는 또한 ImportC 코드가 다른 C 파일들도 직접 import할 수 있게 해 줘요. 그들도 .h 파일을 만들 필요가 없어요. import된 C 함수는 인라인할 수 있게 돼요.

ImportList는 D에서와 똑같이 동작해요.

CImportDeclaration의 순서는 의미가 없어요.

ImportC 파일을 import할 수 있어요. import할 C 파일의 이름은 모듈 이름에서 유도돼요.

ImportC 파일의 모든 전역 심볼은 import하는 모듈에서 쓸 수 있게 돼요.

import하는 파일에서 어떤 이름을 찾지 못하면, 각 import된 파일의 전역 심볼에서 그 이름을 찾아요. 정확히 한 모듈에서 찾으면 그게 이름의 해석이 되고, 여러 모듈에서 찾으면 오류예요.

import된 모듈의 전처리기 심볼은 import하는 모듈에서 쓸 수 없고, import하는 파일의 전처리기 심볼도 import된 모듈에서 쓸 수 없어요.

D 모듈은 ImportDeclaration과 같은 방식으로 import할 수 있어요.

Import는 순환할 수 있어요.

__import core.stdc.stdarg; // get D declaration of va_list
__import mycode;           // import mycode.c

int foo()
{
    va_list x;    // picks up va_list from core.stdc.stdarg
    return 1 + A; // returns 4
}

mycode.c는 이렇게 생겼어요:

enum E { A = 3; }

파일 끝의 Control Z (Control Z is End Of File)

소스 텍스트의 control-Z 문자 \x1A는 파일 끝(End Of File)을 뜻해요.

long long보다 큰 부호 있는 정수 리터럴 (Signed Integer Literal Larger Than long long)

접미사가 없고 long long 타입보다 크지만 unsigned long long 타입에는 들어맞는 부호 있는 정수 상수는 받아들여지고 unsigned long long으로 타입이 정해져요. 이는 D 동작 및 일부 C 컴파일러의 동작과 일치해요.

점과 화살표 연산자 (Dot and Arrow Operators)

. 연산자는 struct나 union 값의 멤버를 지정하는 데 쓰여요. -> 연산자는 포인터가 가리키는 struct나 union 값의 멤버를 지정하는 데 쓰이고요. 확장 기능은 .->를 값과 포인터에 서로 바꿔 쓸 수 있다는 거예요. 이는 .에 대한 D의 동작과 일치해요.

Gnu 및 Clang 확장 (Gnu and Clang Extensions)

gcc와 clang은 확장에 관해 동일한 동작을 가진다고 가정하므로, 여기서 gcc는 둘 다를 가리켜요.

attribute 확장 (attribute Extensions)

다음 __attribute__ 확장들:

  • __attribute__((aligned(N)))
  • __attribute__((always_inline))
  • __attribute__((deprecated))
  • __attribute__((dllexport))
  • __attribute__((dllimport))
  • __attribute__((naked))
  • __attribute__((noinline))
  • __attribute__((noreturn))
  • __attribute__((nothrow))
  • __attribute__((pure))
  • __attribute__((vector_size(N)))
  • 그 외에는 무시돼요.

attribute((noreturn))

__attribute__((noreturn))은 함수가 절대 반환하지 않는다는 걸 표시해요. gcc는 이것을 함수의 속성으로 설정하지, 함수 타입의 일부로는 설정하지 않아요. D에서 절대 반환하지 않는 함수는 반환 타입이 noreturn이에요. 그 차이는 다음 코드에서 볼 수 있어요.

attribute((noreturn)) int foo();
size_t x = sizeof(foo());

이 코드는 gcc에서 받아들여지지만 D에서는 말이 안 돼요. 따라서 ImportC에서는 동작하지만 D 코드로는 표현할 수 없어서, C noreturn 함수와 인터페이스할 .di 파일을 만들 때는 판단을 잘해야 한다는 뜻이에요.

게다가 D 컴파일러는 도달할 수 없는 코드(unreachable code)에 대해 컴파일 타임 오류를 내는 방식으로 noreturn 함수를 활용해요. 하지만 그런 도달할 수 없는 코드는 C11에서 유효하고, ImportC 컴파일러는 그것을 받아들여요.

Visual C 확장 (Visual C Extensions)

Digital Mars C 확장 전부를 지원해요.

MSVC의 컴파일러 내장 함수 중 많은 것을 지원해요.

__stdcall 함수 호출 규약 (__stdcall Function Calling Convention)

__stdcall은 함수의 호출 규약을 Windows API 호출 규약으로 설정해요.

int __stdcall foo(int x);

__declspec 속성 확장 (__declspec Attribute Extensions)

다음 __declspec 확장들:

  • __declspec()
  • __declspec(align(N))
  • __declspec(deprecated)
  • __declspec(dllexport)
  • __declspec(dllimport)
  • __declspec(naked)
  • __declspec(noinline)
  • __declspec(noreturn)
  • __declspec(nothrow)
  • __declspec(thread)
  • 그 외에는 무시돼요.

__pragma 속성 확장 (__pragma Attribute Extensions)

다음 __pragma 확장들:

  • __pragma(pack(N))
  • 그 외에는 무시돼요.

Digital Mars C 확장 (Digital Mars C Extensions)

__stdcall 함수 호출 규약 (__stdcall Function Calling Convention)

__declspec 속성 확장 (__declspec Attribute Extensions)

다음 __declspec 확장들:

  • __declspec(dllexport)
  • __declspec(dllimport)
  • __declspec(naked)
  • __declspec(thread)

MSVC 내장 함수 (MSVC Intrinsics)

MSVC의 컴파일러 내장 함수 중 많은 것이 ImportC에서 지원돼요. 다 나열하기엔 너무 많아서, 더 자세한 내용은 src/__builtins_msvc.d를 참고하세요.

D 관점에서 본 ImportC (ImportC from D's Point of View)

C 구문을 D 구문에 일대일로 대응시키는 건 불가능하지만, 아주 가깝긴 해요. 다음은 D 쪽에서 import된 C 선언들을 어떻게 보는지에 대한 설명이에요.

모듈 이름 (Module Name)

ImportC 파일에 할당되는 모듈 이름은 파일명에서 경로와 확장자를 뗀 이름이에요. 이는 모듈 선언이 없는 D 모듈에 할당되는 기본 모듈 이름과 똑같아요.

extern (C)

모든 C 심볼은 extern (C)예요.

열거형 (Enums)

C 열거형:

enum E { A, B = 2 };

D 코드에는 이렇게 보여요.

enum E : int { A, B = 2 }
alias A = E.A;
alias B = E.B;

.min.max 속성을 쓸 수 있어요.

static assert(E.min == 0 && E.max == 2);

태그 심볼 (Tag Symbols)

태그 심볼은 struct, union, enum 키워드 뒤에 오는 식별자예요(C11 6.7.2.3). C에서 이것들은 다른 식별자들과 다른 심볼 테이블에 놓여요. 즉 서로 다른 두 심볼이 같은 이름을 쓸 수 있다는 뜻이에요.

int S;
struct S { int a, b; };
S = 3;
struct S *ps;

D는 이런 구별을 하지 않아요. 태그 심볼이 식별자의 유일한 선언이면 D 컴파일러가 그것을 인식해요. 태그 심볼과 비태그 심볼이 식별자를 공유하면 D 컴파일러는 비태그 심볼을 인식해요. C에서 흔히 typedef를 적용하는 관례 덕분에 보통은 문제가 되지 않아요.

typedef struct S { int a, b; } S;

D 컴파일러는 S에 적용된 typedef를 인식하고 코드가 예상대로 컴파일돼요. 하지만 typedef가 없는 경우, 예를 들면:

int S;
struct S { int a, b; };

가장 실용적인 해결책은 C 코드에 typedef를 추가하는 거예요.

int S;
struct S { int a, b; };
typedef struct S S_t;    // add this typedef

그러면 D 컴파일러가 S_t를 통해 struct 태그 심볼에 접근할 수 있어요.

C 코드 감싸기 (Wrapping C Code)

C 코드를 ImportC에 맞게 적응시키는 데 따르는 많은 어려움은 C 코드 자체를 편집하지 않고도 해결할 수 있어요. C 코드를 다른 C 파일로 감싼 다음 #include하면 돼요. 다음의 문제 있는 C 파일 file.c를 생각해 볼게요.

void func(int *__restrict p);
int S;
struct S { int a, b; };

문제는 __restrict가 ImportC(C11도)가 인식하는 타입 한정자가 아니라는 점, 그리고 struct Sint S; 선언에 가려져 D에서 보이지 않는다는 점이에요. file.c를 수정해 감싸려면 내용이 이러헌 file_ic.c 파일을 만들어요.

#define __restrict restrict
#include "file.c"
typedef struct S S_t;

그런 다음 import file; 대신 import file_ic;를 쓰고, struct S가 필요할 때는 S_t를 사용하면 돼요.

C 코드를 D 코드로 변환하기 (Converting C Code to D Code)

때로는 C 코드를 import하는 것보다 더 나아가, 실제로 C 소스를 D 소스로 변환하는 게 바람직할 때가 있어요. 그 이유는 다음과 같아요.

  • C 프로젝트를 D 프로젝트로 이전하기 위해.
  • 전처리기가 필요 없는 등의 이유로 동등한 D 코드가 훨씬 빨리 컴파일될 수 있으므로.
  • 메모리 안전성, 순수성 등의 속성을 추가하도록 D 코드를 다듬기 위해.
  • 프로젝트의 D 부분에서 C-ism을 없애기 위해.

이것은 D 컴파일러의 -Hf 스위치로 할 수 있어요.

dmd -c mycode.c -Hf=mycode.di

이 스위치는 mycode.c의 C 소스 코드를 mycode.di의 D 소스 코드로 변환해 줘요. -inline 스위치도 함께 쓰면 함수 원형(prototype)만 내보내는 대신 C 함수 본문도 함께 내보내 줘요.

임피던스 불일치 (Impedance Mismatch)

모든 기묘함을 포함한 C 의미론을 D 소스 코드로 정밀하게 매핑하는 게 항상 실용적이진 않아요. ImportC는 자신의 의미 분석에서 C 의미론을 사용해서, D 소스 코드로 표현할 수 있는 것보다 훨씬 정확한 C 의미론에 가까워져요. 따라서 D 소스 코드로의 변환은 완벽보다는 못할 거예요. 예를 들면:

int S;
struct S { int a, b; };
int foo(struct S s)
{
    return S + s.a;
}

이건 int Sstruct S가 서로 다른 심볼 테이블에 있기 때문에 ImportC에서 잘 동작해요. 하지만 생성된 D 코드에서는 두 심볼이 같은 심볼 테이블에 있어서 충돌할 거예요. C에서 변환된 그런 D 소스 코드는 사용자가 손봐야 해요.

그럼에도 현장의 보고에 따르면, 이 변환 기능은 기존 C 코드를 다뤄야 하는 사용자에게 큰 시간 절약이 된다고 해요.

경고 (Warnings)

일반적인 컴파일러가 보통 기본으로 경고를 내는 의심스러운 C 구문이 많아요. 예를 들면:

int *p = 3; // Warning: integer implicitly converted to pointer

ImportC는 경고를 내지 않아요. 사용자가 다른 C 컴파일러로 개발한 기존 C 코드를 import할 것이고, 그 코드는 의도한 대로 쓰였다고 가정하기 때문이에요. C11이 합법이라고 하면 ImportC는 받아들여요.

ImportC++

ImportC는 C++ 코드를 컴파일하지 않아요. 그런 용도는 dpp를 쓰세요.

다른 해결책 (Other Solutions)

Atila Neves의 dpp

dpp 코드

dpp 기사

기사에서:

dpp는 컴파일러 래퍼로, .dpp 확장자를 가진 D 소스 파일을 파싱하고 마주치는 모든 #include 지시어를 제자리에서 확장하며, 모든 C나 C++ 심볼을 D로 변환한 다음 결과를 D 컴파일러(기본은 DMD)에 넘겨줘요.

DStep처럼 dpp는 libclang에 의존해요.

Jacob Carlborg의 DStep

DStep 코드

DStep 기사

기사에서:

DStep은 C 및 Objective-C 라이브러리용 D 바인딩을 자동 생성하는 도구예요. C나 Objective-C 헤더 파일을 처리하고 D 모듈을 출력하는 방식으로 구현돼요. DStep은 Clang 컴파일러를 라이브러리(libclang)로 사용해서 헤더 파일을 처리해요.

Walter Bright의 htod

htod는 C .h 파일을 D 소스 파일로 변환해 주는데, 그 결과를 D 코드에 import하기에 적합해요. htod는 Digital Mars C 및 C++ 컴파일러의 프런트 엔드로 만들어졌어요. C나 C++ 컴파일러처럼 동작하되, 객체 코드 대신 D 모듈용 소스 코드를 출력한다는 점만 달라요.

ImportC의 작동 방식 (How ImportC Works)

ImportC의 구현은 D의 의미론이 C의 것과 아주 비슷하다는 아이디어에 기반해요. ImportC는 자신만의 파서를 갖고 있는데, 이 파서가 C 문법을 D가 쓰는 것과 같은 AST(Abstract Syntax Tree)로 변환해요. ImportC의 렉서는 D와 같지만, 키워드와 정수 리터럴이 다르다는 점 같은 것들이 여기저기 수정되어 있어요. C의 의미론이 D와 다른 곳에서는 D 컴파일러의 의미 분석 코드에 조정이 있어요.

이렇게 D 의미론 구현을 가져다 쓰는 방식 덕분에 ImportC는 전방 참조 처리, CTFE(Compile Time Function Execution), C 함수를 D 코드로 인라이닝하는 일 같은 것들을 할 수 있어요. 전방 참조를 처리할 수 있다는 건 C 선언을 D로 import하기 위해 .h 파일조차 쓸 필요가 없다는 뜻이에요. CTFE를 수행할 수 있다는 건 실행 파일을 생성하지 않고 ImportC가 제대로 동작하는지 테스트하기에 아주 편리해요. 하지만 전반적으로 ImportC에 D 기능을 추가하고 싶은 강한 유혹에는 저항해 왔어요.

물론 최적화기와 코드 생성기는 D가 쓰는 것과 같아요.

더 알아보기 (Learn more)

  • D 언어 공식 사양 원문: ImportC
  • Better C와의 관계를 보려면: Better C
  • Live Functions: ob