ImportC
ImportC
D 언어 구현 안에 내장되어 있는 C 컴파일러인 ImportC에 대해, 작동 방식부터 지원 범위까지 차근차근 살펴볼게요. ImportC는 C 파일을 그대로 가져와 D 코드와 함께 링크해서 실행 파일을 만들 수 있게 해 주는 녀석이에요.
본문
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):foo나bar라는 이름의 C 함수 선언을 무시해요.#pragma function_decl(consider, foo, bar):foo나bar라는 이름의 C 함수 선언을 고려(무시를 중단)해요.#pragma function_def(ignore, foo, bar):foo나bar라는 이름의 C 함수 정의를 무시해요.#pragma function_def(consider, foo, bar):foo나bar라는 이름의 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는 절대 예외를 던지지 않는다고 가정해요. setjmp와 longjmp는 지원하지 않아요.
const
C11은 const가 지역적으로만 적용된다고 규정해요. ImportC에서 const는 전이적으로(transitively) 적용돼요. 즉:
int *const p;
이 코드가 C11에서는 p가 int를 가리키는 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의 long과 unsigned long이 int와 unsigned int와 각각 같은 크기예요. 다른 플랫폼에서는 C의 long과 unsigned long이 long long과 unsigned long long과 같은 크기이고요. long double과 long double _Complex는 double과 double _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 S가 int 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 S와 struct 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확장자를 가진 D 소스 파일을 파싱하고 마주치는 모든#include지시어를 제자리에서 확장하며, 모든 C나 C++ 심볼을 D로 변환한 다음 결과를 D 컴파일러(기본은 DMD)에 넘겨줘요.
DStep처럼 dpp는 libclang에 의존해요.
Jacob Carlborg의 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가 쓰는 것과 같아요.