프라그마(Pragmas)

프라그마(Pragmas)

D에서 pragma컴파일러(구현)에게 특별한 정보를 전달하는 문법이에요. 일반 코드처럼 실행되는 게 아니라, "이 함수는 링커에게 이 라이브러리를 붙여줘", "이 함수는 시작 시점에 자동으로 실행돼줘" 같은 지시를 전달하는 역할을 하죠. 이런 지시문(Pragma)을 잘 활용하면 바이너리 레벨의 세밀한 조정이나 벤더 특유의 확장도 자연스럽게 더할 수 있어요.

이번 챕터에서는 pragma가 어떤 모양으로 쓰이는지, 그리고 D가 미리 정의해 둔 프라그마들이 각각 무엇을 하는지 하나씩 살펴볼게요.

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

본문

Pragmas

Pragma는 다음과 같은 구조를 가져요. 문법 규칙부터 볼게요.

PragmaDeclaration:
    Pragma ;
    Pragma DeclarationBlock

PragmaStatement:
    Pragma ;
    Pragma NoScopeStatement

Pragma:
    pragma ( Identifier )
    pragma ( Identifier , ArgumentList )

프라그마는 구현(구현체)에게 특별한 정보를 전달하고, 벤더 특유의 확장을 추가할 수도 있어요. 프라그마는 단독으로 쓰되 ;로 끝내거나, 하나의 문장(statement), 문장 블록, 하나의 선언(declaration), 또는 선언 블록에 적용할 수 있어요.

프라그마는 PragmaDeclaration 또는 PragmaStatement 둘 중 하나의 형태예요.

pragma(ident);        // just by itself

pragma(ident) declaration; // influence one declaration

pragma(ident): // influence subsequent declarations
    declaration;
    declaration;

pragma(ident)   // influence block of declarations
{
    declaration;
    declaration;
}

pragma(ident) statement; // influence one statement

pragma(ident)   // influence block of statements
{
    statement;
    statement;
}

프라그마의 종류는 Identifier로 결정돼요. ArgumentList는 콤마로 구분된 AssignExpression(대입식)들의 목록이고, 각 AssignExpression은 문법적으로는 식(expression)으로 파싱될 수 있어야 해요. 다만 그 의미가 무엇인지는 개별 프라그마의 시맨틱스에 달려 있죠.

Predefined Pragmas

모든 구현은 다음 프라그마들을 지원해야 해요. 설령 그냥 무시하기만 하더라도 말이죠:

  • pragma crt_constructor
  • pragma crt_destructor
  • pragma inline
  • pragma lib
  • pragma linkerDirective
  • pragma mangle
  • pragma msg
  • pragma printf
  • pragma scanf
  • pragma startaddress

구현 정의(Implementation Defined): 구현이 이러한 프라그마들을 무시해도 됩니다.

pragma crt_constructor

함수에 주석을 달아서, 그 함수가 C 런타임 라이브러리가 초기화된 뒤, D 런타임 라이브러리가 초기화되기 전에 실행되도록 해주는 프라그마예요.

이 프라그마로 주석을 단 함수는 반드시 다음 조건을 만족해야 해요:

  1. extern (C)여야 한다.
  2. 매개변수(parameter)를 가져서는 안 된다.
  3. non-static 멤버 함수가 아니어야 한다.
  4. 선언(declaration)이 아니라 함수 정의(definition)여야 한다. 즉 함수 본문(body)을 가져야 한다.
  5. 소멸자(destructor)가 있는 타입을 반환해서는 안 된다.
  6. 중첩 함수(nested function)가 아니어야 한다.
__gshared int initCount;

pragma(crt_constructor)
extern(C) void initializer() { initCount += 1; }

이 프라그마에는 인자를 전달할 수 없어요.

함수는 pragma(crt_constructor)pragma(crt_destructor)를 동시에 달 수도 있어요.

함수 정의가 아닌 다른 선언에 이 프라그마를 달아도 아무 효과가 없어요.

구조체(struct)나 클래스(class) 정의에 주석을 단다고 해서 그 애그리게이트(aggregate)의 멤버들까지 영향을 받는 건 아니에요.

pragma(crt_constructor)로 주석이 달린 함수는 constimmutable 변수를 초기화할 수 있어요.

모범 사례(Best Practices): 시스템 프로그래밍이나 C/C++와의 연동에 사용하세요. 예를 들어 DSO를 로드할 때 런타임 초기화를 허용하거나, betterC 모드에서 shared static this의 간단한 대체재로 활용할 수 있어요.

구현 정의(Implementation Defined): pragma(crt_constructor)가 달린 함수들이 실행되는 순서는 구현마다 다릅니다.

모범 사례(Best Practices): 한 모듈 안에서 함수들이 호출되는 순서를 직접 제어하고 싶다면, 원하는 순서대로 그 함수들을 호출하는 단일 함수를 하나 작성하고 그 함수에만 주석을 다세요.

구현 정의(Implementation Defined): 이는 C 컴파일러가 main()이 호출되기 전에 코드를 실행하기 위해 사용하는 메커니즘을 활용합니다. C++ 컴파일러는 이를 사용해 정적 생성자(constructor)와 소멸자(destructor)를 실행하죠. 예를 들어 GCC의 __attribute__((constructor))가 이와 동등합니다. Digital Mars C는 _STI_STD 식별자 접두사를 사용해 crt_constructor 및 crt_destructor 함수를 표시합니다.

구현 정의(Implementation Defined): 주석이 달린 함수에 대한 참조가 어느 섹션에 삽입되는지도 플랫폼마다 다릅니다. Elf 시스템에서는 .init_array 섹션에, Win32 OMF 시스템에서는 XI 섹션에, Windows MSCOFF 시스템에서는 .CRT$XCU 섹션에, OSX 시스템에서는 __mod_init_func 섹션에 삽입됩니다.

참고(Note): crt_constructorcrt_destructor는 v2.078.0(2018-01-01)에서 구현됐어요. 그 전에는 일부 컴파일러가 비표준적이고 컴파일러 특유의 메커니즘을 노출하고 있었죠.

pragma crt_destructor

pragma(crt_destructor)pragma(crt_constructor)와 같은 방식으로 동작하되, 다음 두 가지만 달라요:

  1. 함수에 주석을 달아서, 그 함수가 D 런타임 라이브러리가 종료된 뒤, C 런타임 라이브러리가 종료되기 전에 실행되도록 해준다. C의 exit() 함수를 호출해도 주석이 달린 함수들이 실행된다.
  2. 주석이 달린 함수들이 실행되는 순서는 pragma(crt_constructor)가 달린 함수들의 역순이다.

구현 정의(Implementation Defined): 이는 C 컴파일러가 main()이 반환된 후 또는 exit()가 호출된 후 코드를 실행하기 위해 사용하는 메커니즘을 활용합니다. C++ 컴파일러는 이를 사용해 정적 소멸자를 실행하죠. 예를 들어 GCC의 __attribute__((destructor))가 이와 동등합니다. Digital Mars C는 _STI_STD 식별자 접두사를 사용해 crt_constructor 및 crt_destructor 함수를 표시합니다.

구현 정의(Implementation Defined): 주석이 달린 함수에 대한 참조는 Elf 시스템의 .fini_array 섹션, Win32 OMF 시스템의 XC 섹션, Windows MSCOFF 시스템의 .CRT$XPU 섹션, OSX 시스템의 __mod_term_func 섹션에 삽입됩니다.

__gshared int initCount;

pragma(crt_constructor)
extern(C) void initialize() { initCount += 1; }

pragma(crt_destructor)
extern(C) void deinitialize() { initCount -= 1; }

pragma(crt_constructor)
pragma(crt_destructor)
extern(C) void innuendo() { printf("Inside a constructor... Or destructor?\n"); }

pragma inline

함수가 인라인(inline)될지 여부에 영향을 주는 프라그마예요. 선언 레벨에서 쓰면 자신이 제어하는 블록 안에 선언된 함수들에 영향을 주고, 함수 내부에서 쓰면 자신을 감싸고 있는 함수에 영향을 줘요.

pragma(inline)에는 두 가지 형태가 있어요:

pragma(inline)

구현의 기본 동작과 일치하도록 동작을 설정해요.

pragma(inline, AssignExpression)

AssignExpression이 평가되어 불리언(boolean)으로 변환 가능한 타입이어야 해요. 결과가 false면 함수는 절대 인라인되지 않고, 그렇지 않으면 항상 인라인돼요.

AssignExpression을 둘 이상 사용할 수는 없어요.

한 함수 안에 인라인 프라그마가 여러 개 있으면, 어휘적으로 가장 마지막에 있는 것이 적용돼요.

pragma(inline):
int foo(int x) // foo() is never inlined
{
    pragma(inline, true);
    ++x;
    pragma(inline, false); // supercedes the others
    return x + 3;
}

구현 정의(Implementation Defined): 1. 기본적인 인라인 동작은 보통 -inline 같은 컴파일러 스위치로 선택할 수 있습니다.

  1. 특정 함수가 인라인될 수 있는지 여부는 구현마다 다릅니다.
  2. 함수를 인라인할 수 없는데 pragma(inline, true)를 했을 때 어떻게 될지도 구현마다 다릅니다. 보통은 오류 메시지가 나옵니다.

pragma lib

AssignExpression이 정확히 하나 있어야 하고, 컴파일 타임에 문자열 리터럴로 평가되어야 해요.

pragma(lib, "foo.lib");

구현 정의(Implementation Defined): 문자열 리터럴은 라이브러리 파일의 파일 이름을 지정합니다. 이 이름은 생성된 오브젝트 파일에 삽입되거나, 그렇지 않으면 링커에 전달되어 링커가 그 라이브러리를 자동으로 링크하게 합니다.

pragma linkerDirective

AssignExpression이 정확히 하나 있어야 하고, 컴파일 타임에 문자열 리터럴로 평가되어야 해요.

pragma(linkerDirective, "/FAILIFMISMATCH:_ITERATOR_DEBUG_LEVEL=2");

구현 정의(Implementation Defined): 문자열 리터럴은 생성된 오브젝트 파일에 내장될 링커 지시문(linker directive)을 지정합니다. 링커 지시문은 MS-COFF 출력에서만 지원됩니다.

pragma mangle

심볼(symbol)의 기본 맹글링(mangling, 이름 규칙 변환)을 재정의(override)하는 프라그마예요.

변수와 함수의 경우 AssignExpression이 정확히 하나 있어야 하고 컴파일 타임에 문자열 리터럴로 평가되어야 해요. 애그리게이트(aggregate)의 경우 AssignExpression이 하나 또는 둘일 수 있는데, 그중 하나는 컴파일 타임에 문자열 리터럴로 평가되어야 하고, 하나는 심볼로 평가되어야 해요. 그 심볼이 TemplateInstance라면, 그 애그리게이트는 그 TemplateInstance의 시그니처와 인자를 가진 템플릿으로 취급돼요. 문자열이 제공되지 않으면 심볼의 식별자가 사용돼요. 애그리게이트의 이름이 D 키워드인 경우에는 두 인자를 모두 사용할 수 있어요.

이 프라그마는 함수와 변수 심볼에만 적용돼요. 다른 심볼들은 무시돼요.

구현 정의(Implementation Defined): macOS와 Win32에서는 2.079 이후로 C/C++ 툴체인이 하듯이 문자열 앞에 밑줄(_) 하나가 추가됩니다. 덕분에 플랫폼마다 특수 처리를 하지 않고도 모든 호환 플랫폼(한쪽은 POSIX, 다른 쪽은 win64)에서 같은 pragma(mangle)을 사용할 수 있어요.

구현 정의(Implementation Defined): macOS에서는 2.113 이후로, 심볼 이름이 section$로 시작하면 앞에 오는 밑줄(_)이 자동으로 생성되지 않습니다. 이 이름은 macOS 링커가 섹션용으로 생성하는 특수 이름으로, 앞에 밑줄로 시작하지 않기 때문이에요.

이유(Rationale): - D로는 표현할 수 없는 심볼 이름에 링크할 수 있게 합니다.

  • D 키워드인 심볼에 링크할 수 있게 합니다. Identifier는 키워드가 될 수 없기 때문이에요.
pragma(mangle, "body")
extern(C) void body_func();
pragma(mangle, "function")
extern(C++) struct _function {}
template ScopeClass(C)
{
    pragma(mangle, C)
    struct ScopeClass { align(__traits(classInstanceAlignment, C)) void[__traits(classInstanceSize, C)] buffer; }
}
extern(C++)
{
    class MyClassA(T) {}
    void func(ref ScopeClass!(MyClassA!int)); // mangles as MyClass<int>&
}

pragma msg

AssignExpression이 컴파일 타임에 평가되고, 모두 하나의 메시지로 합쳐져요.

pragma(msg, "compiling...", 6, 1.0); // prints "compiling...61.0" at compile time

구현 정의(Implementation Defined): 메시지가 어떤 형태를 띠고 사용자에게 어떻게 제시되는지는 구현마다 다릅니다. 한 가지 방법은 표준 오류 흐름(standard error stream)에 출력하는 것입니다.

이유(Rationale): 런타임에 정보성 메시지를 쓰는 역할을 writeln()이 한다면, 컴파일 타임에는 pragma(msg)가 그와 동등한 역할을 합니다. 예를 들어,

static if (kilroy)
    pragma(msg, "Kilroy was here");
else
    pragma(msg, "Kilroy got lost");

pragma printf

pragma(printf)는 함수 선언이 printf와 같은 함수임을 지정해요. 즉 extern (C) 또는 extern (C++) 함수로, C99 표준 7.19.6.1을 따르는 0으로 끝나는 char 문자열에 대한 포인터를 받는 format 매개변수를 가지며, 그 바로 뒤에 ... 가변 인자 목록(variadic argument list) 또는 마지막 매개변수로 va_list 타입의 매개변수가 오는 함수라는 뜻이에요.

format 인자가 문자열 리터럴이면, C99 표준에 따라 유효한 포맷 문자열인지 검증돼요. format 매개변수 뒤에 ...이 오면, 가변 인자의 개수와 타입이 포맷 문자열과 대조되어 검사돼요.

진단되는 비호환성(diagnosed incompatibilities)은 다음과 같아요:

  • 인자 정렬을 어긋나게 할 수 있는 호환되지 않는 크기
  • 포인터가 아닌 인자를 역참조(dereferencing)하는 경우
  • 인자 개수가 부족한 경우
  • 구조체 인자
  • 배열과 슬라이스 인자
  • s 지정자에 non-pointer 인자를 쓰는 경우
  • 비표준 포맷
  • C99 기준의 미정의 동작(undefined behavior)

C99 표준에 따르면, 초과 인자(extra arguments)는 무시돼요.

무시되는 불일치(ignored mismatches)는 다음과 같아요:

  • 부호 불일치. 예를 들어 %u 포맷으로 int를 출력하는 경우
  • 정수 승격(integral promotion) 불일치. 포맷이 intuint보다 작은 정수 타입을 지정하는데, %hd 대신 %dshort를 출력하는 경우처럼요
printf("%k\n", value); // error: non-Standard format k
printf("%d\n");        // error: not enough arguments
printf("%d\n", 1, 2);  // ok, extra arguments ignored

모범 사례(Best Practices): non-Standard printf/scanf 포맷을 쓰려면 다음과 같은 간단한 우회 방법이 있어요:

const format = "%k\n";
printf(format.ptr, value);  // no error

모범 사례(Best Practices): 감지되는 오류의 대부분은 이식성(portability) 문제예요. 예를 들어,

string s;
printf("%.*s\n", s.length, s.ptr);
printf("%d\n", s.sizeof);
ulong u;
scanf("%lld%*c\n", &u);

이 코드는 다음과 같이 바꿔야 해요:

string s;
printf("%.*s\n", cast(int) s.length, s.ptr);
printf("%zd\n", s.sizeof);
ulong u;
scanf("%llu%*c\n", &u);

함수가 아닌 선언에 pragma(printf)를 적용하면 무시돼요. 특히 함수 포인터 타입의 선언에는 아무 효과가 없어요.

pragma scanf

pragma(scanf)는 함수 선언이 scanf와 같은 함수임을 지정해요. 즉 extern (C) 또는 extern (C++) 함수로, C99 표준 7.19.6.2를 따르는 0으로 끝나는 char 문자열에 대한 포인터를 받는 format 매개변수를 가지며, 그 바로 뒤에 ... 가변 인자 목록 또는 마지막 매개변수로 va_list 타입의 매개변수가 오는 함수라는 뜻이에요.

format 인자가 문자열 리터럴이면, C99 표준에 따라 유효한 포맷 문자열인지 검증돼요. format 매개변수 뒤에 ...이 오면, 가변 인자의 개수와 타입이 포맷 문자열과 대조되어 검사돼요.

진단되는 비호환성은 다음과 같아요:

  • 인자가 포맷이 지정한 타입에 대한 포인터가 아닌 경우
  • 인자 개수가 부족한 경우
  • 비표준 포맷
  • C99 기준의 미정의 동작

C99 표준에 따르면, 초과 인자는 무시돼요.

함수가 아닌 선언에 pragma(scanf)를 적용하면 무시돼요. 특히 함수 포인터 타입의 선언에는 아무 효과가 없어요.

pragma startaddress

AssignExpression이 정확히 하나 있어야 하고, 컴파일 타임에 함수 심볼로 평가되어야 해요.

구현 정의(Implementation Defined): 함수 심볼은 프로그램의 시작 주소(start address)를 지정합니다. 그 심볼은 오브젝트 파일에 삽입되거나, 그렇지 않으면 시작 주소를 설정하도록 링커에 제시됩니다. 이것은 보통 애플리케이션 레벨 프로그래밍에는 사용되지 않고, 특수화된 시스템 작업에 사용됩니다. 애플리케이션 코드에서는 시작 주소를 런타임 라이브러리가 처리해 주기 때문이에요.

void foo() { ... }
pragma(startaddress, foo);

Vendor Specific Pragmas

벤더 특유의 pragma Identifier는, 버전 식별자(version identifier)와 비슷하게, 벤더의 상표명(trademarked name)을 접두사로 붙이면 정의할 수 있어요:

pragma(DigitalMars_extension) { ... }

구현은 인식되지 않는 Pragma에 대해서는 벤더 특유의 것이라 할지라도 오류를 진단해야 해요.

구현 정의(Implementation Defined): 벤더 특유의 프라그마.

모범 사례(Best Practices): 벤더 특유의 프라그마는 version 문장으로 감싸는 것이 좋아요.

version (DigitalMars)
{
    pragma(DigitalMars_extension)
    {   ... }
}

더 알아보기 (Learn more)