구현 특정 프라그마
구현 특정 프라그마 (Implementation Specific Pragmas)
이 절에서는 현재 Nim 구현체가 지원하지만 언어 명세(specification)의 일부로 간주해서는 안 되는 추가 프라그마들을 설명해요.
Bitsize 프라그마
bitsize 프라그마는 객체 필드 멤버를 위한 거예요. 필드를 C/C++의 비트 필드(bitfield)로 선언합니다.
type
mybitfield = object
flag {.bitsize:1.}: cuint
위 코드는 다음 C 코드를 생성해요:
struct mybitfield {
unsigned int flag:1;
};
size 프라그마
Nim은 열거형(enum)의 크기를 자동으로 결정해요. 그런데 C 열거형 타입을 래핑할 때는 특정 크기가 필요하죠. size 프라그마가 열거형 타입의 크기를 지정할 수 있게 해줍니다.
type
EventType* {.size: sizeof(uint32).} = enum
QuitEvent,
AppTerminating,
AppLowMemory
doAssert sizeof(EventType) == sizeof(uint32)
열거형 타입에 사용할 때 size 프라그마는 1, 2, 4, 8 값만 받아요.
size 프라그마는 importc 불완전 객체 타입(incomplete object type)의 크기도 지정할 수 있어요. 그 덕분에 필드 없이 선언된 타입이라도 컴파일 타임에 크기를 알 수 있죠.
type
AtomicFlag* {.importc: "atomic_flag", header: "<stdatomic.h>", size: 1.} = object
static:
# AtomicFlag에 size 프라그마가 없었다면 이 코드는 컴파일 타임 오류가 났을 거예요.
echo sizeof(AtomicFlag)
Align 프라그마
align 프라그마는 변수와 객체 필드 멤버를 위한 거예요. 선언되는 대상의 정렬 요구사항(alignment requirement)을 바꿉니다. 인자는 2의 거듭제곱인 상수여야 해요. 같은 선언에 붙은 다른 align 프라그마보다 약한 정렬은 무시되고, 타입이 요구하는 정렬보다 약한 정렬도 무시돼요.
type
sseType = object
sseData {.align(16).}: array[4, float32]
# 모든 객체가 128바이트 경계로 정렬됩니다
Data = object
x: char
cacheline {.align(128).}: array[128, char] # 과정렬(over-aligned)된 char 배열
proc main() =
echo "sizeof(Data) = ", sizeof(Data), " (1 byte + 127 bytes padding + 128-byte array)"
# 출력: sizeof(Data) = 256 (1 byte + 127 bytes padding + 128-byte array)
echo "alignment of sseType is ", alignof(sseType)
# 출력: alignment of sseType is 16
var d {.align(2048).}: Data # 이 Data 인스턴스는 더 엄격하게 정렬됩니다
main()
이 프라그마는 JavaScript 백엔드에는 아무 영향이 없고, --mm:refc 옵션에서는 메모리 사용량이 크게 늘어날 수 있어요.
Noalias 프라그마
Nim 컴파일러 1.4 버전부터 변수와 매개변수에 .noalias 주석을 달 수 있어요. 이 주석은 C/C++의 restrict 키워드에 그대로 매핑되는데, 바탕이 되는 포인터가 메모리의 고유한 위치를 가리키고, 그 위치를 가리키는 다른 별칭(alias)이 없다는 뜻이에요. 이 별칭 제한이 지켜지는지는 검사되지 않아요. 만약 제한을 어기면 백엔드 최적화기가 코드를 잘못 컴파일해도 자유입니다. 이건 안전하지 않은(unsafe) 언어 기능이에요.
이상적으로는 언어의 이후 버전에서 이 제한이 컴파일 타임에 강제되겠죠. (그래서 unsafeAssumeNoAlias 같은 더 긴 이름 대신 noalias라는 이름을 고른 이유이기도 해요.)
Volatile 프라그마
volatile 프라그마는 변수에만 쓸 수 있어요. 변수를 C/C++에서의 volatile — 그 의미가 C/C++에서도 잘 정의되어 있지 않지만 — 로 선언합니다.
참고: 이 프라그마는 LLVM 백엔드에는 존재하지 않을 거예요.
nodecl 프라그마
nodecl 프라그마는 거의 모든 심볼(변수, 프로시저, 타입 등)에 적용할 수 있고, C와 상호운용할 때 가끔 유용해요. Nim에게 C 코드에서 그 심볼에 대한 선언을 생성하지 말라고 알려줍니다. 예를 들면:
var
EACCES {.importc, nodecl.}: cint # EACCES를 변수인 것처럼 취급합니다. Nim은 그 값을 모르거든요.
그래도 대개는 header 프라그마가 더 나은 선택이에요.
참고: 이것은 LLVM 백엔드에서는 동작하지 않아요.
Header 프라그마
header 프라그마는 nodecl 프라그마와 매우 비슷해요. 거의 모든 심볼에 적용할 수 있고, 심볼을 선언하지 말고 대신 생성된 코드에 #include를 넣도록 지정합니다.
type
PFile {.importc: "FILE*", header: "<stdio.h>".} = distinct pointer
# C의 FILE* 타입을 import합니다; Nim은 이를 새로운 포인터 타입으로 취급해요
header 프라그마는 항상 문자열 상수를 기대해요. 그 문자열 상수가 헤더 파일을 담고 있죠. C에서 늘 그렇듯 시스템 헤더 파일은 꺾쇠 괄호 <>로 감쌉니다. 꺾쇠 괄호가 없으면 Nim은 생성된 C 코드에서 헤더 파일을 ""로 감싸요.
참고: 이것은 LLVM 백엔드에서는 동작하지 않아요.
IncompleteStruct 프라그마
incompleteStruct 프라그마는 컴파일러에게 sizeof 표현식에서 바탕이 되는 C struct를 사용하지 말라고 알려줍니다.
type
DIR* {.importc: "DIR", header: "<dirent.h>",
pure, incompleteStruct.} = object
incompleteStruct 타입에 컴파일 타임에 sizeof를 쓰려고 하면 "'sizeof' cannot be used with '.incompleteStruct' types" 오류가 나요.
CompleteStruct 프라그마
completeStruct 프라그마는 계약(contract)으로, importc 타입 선언이 해당 C 타입의 모든 필드를 담고 있음을 나타내요. 그래서 sizeof, alignof, offsetof를 컴파일 타임에 계산할 수 있게 됩니다.
기본적으로 importc 타입은 불완전한 것으로 간주돼요(컴파일 타임에 크기를 알 수 없죠). 컴파일 타임 크기 정보가 필요하고, Nim 정의가 C 레이아웃과 일치함을 보장할 수 있을 때 completeStruct를 쓰면 돼요:
type
InotifyEvent {.importc: "struct inotify_event", header: "<sys/inotify.h>",
completeStruct.} = object
wd: cint
mask: uint32
cookie: uint32
len: uint32
# 모든 필드가 C 구조체와 정확히 일치해야 합니다
Nim 필드가 C 구조체와 일치하지 않으면 C 코드 생성 중에 정적 단언(static assertion)이 실패해요.
completeStruct 없이 컴파일 타임에 importc 타입에 sizeof를 쓰려고 하면 "'sizeof' requires '.importc' types to be '.completeStruct'" 오류가 나요.
Compile 프라그마
compile 프라그마로 C/C++ 소스 파일을 프로젝트와 함께 컴파일하고 링크할 수 있어요.
이 프라그마는 세 가지 형태를 가져요. 첫 번째는 단순한 파일 입력입니다:
{.compile: "myfile.cpp".}
두 번째 형태는 튜플인데, 두 번째 인자가 출력 이름 strutils 포매터예요:
{.compile: ("file.c", "$1.o").}
참고: Nim은 SHA1 체크섬을 계산해서 파일이 바뀌었을 때만 다시 컴파일해요. -f 명령줄 옵션을 쓰면 파일의 재컴파일을 강제할 수 있습니다.
1.4부터는 compile 프라그마를 다음 문법으로도 쓸 수 있어요:
{.compile("myfile.cpp", "--custom flags here").}
예시에서 보듯, 이 새로운 변형은 파일을 재컴파일할 때 C 컴파일러에 전달되는 사용자 지정 플래그를 허용해요.
Link 프라그마
link 프라그마로 프로젝트에 추가 파일을 링크할 수 있어요:
{.link: "myfile.o".}
passc 프라그마
passc 프라그마로 명령줄 스위치 --passc를 쓰는 것처럼 C 컴파일러에 추가 매개변수를 전달할 수 있어요.
{.passc: "-Wall -Werror".}
참고로 시맨틱 분석(semantic analysis) 중에 실행되는 외부 명령에서 매개변수를 끌어오려면 system 모듈의 gorge를 쓸 수 있어요:
{.passc: gorge("pkg-config --cflags sdl").}
localPassC 프라그마
localPassC 프라그마도 C 컴파일러에 추가 매개변수를 전달하지만, 프라그마가 있는 Nim 모듈에서 만들어지는 C/C++ 파일에만 적용돼요:
# Module A.nim
# 생성 결과: A.nim.cpp
{.localPassC: "-Wall -Werror".} # A.nim.cpp를 컴파일할 때만 전달됩니다
passl 프라그마
passl 프라그마로 명령줄 스위치 --passl을 쓰는 것처럼 링커에 추가 매개변수를 전달할 수 있어요.
{.passl: "-lSDLmain -lSDL".}
참고로 시맨틱 분석 중에 실행되는 외부 명령에서 매개변수를 끌어오려면 system 모듈의 gorge를 쓸 수 있어요:
{.passl: gorge("pkg-config --libs sdl").}
Emit 프라그마
emit 프라그마로 컴파일러 코드 생성기의 출력에 직접 영향을 줄 수 있어요. 그렇게 하면 코드가 다른 코드 생성기/백엔드로는 이식할 수 없게 되죠. 사용은 매우 권장하지 않아요! 하지만 C++나 Objective C 코드와 인터페이스할 때는 엄청나게 유용할 수 있습니다.
예시:
{.emit: """
static int cvariable = 420;
""".}
{.push stackTrace:off.}
proc embedsC() =
var nimVar = 89
# 문자열 리터럴 밖의 emit 섹션에서 Nim 심볼에 접근합니다:
{.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""", nimVar, ");"].}
{.pop.}
embedsC()
nimbase.h는 NIM_EXTERNC C 매크로를 정의하는데, extern "C" 코드가 nim c와 nim cpp 둘 다에서 동작하게 만들 때 쓸 수 있어요. 예를 들면:
proc foobar() {.importc:"$1".}
{.emit: """
#include <stdio.h>
NIM_EXTERNC
void fun(){}
""".}
참고: 하위 호환성을 위해,
emit문의 인자가 단일 문자열 리터럴이면 백틱으로 Nim 심볼을 가리킬 수 있어요. 하지만 이 사용법은 deprecated예요.
최상위 emit 문의 경우, 생성된 C/C++ 파일에서 코드가 방출될 위치는 /*TYPESECTION*/, /*VARSECTION*/, /*INCLUDESECTION*/ 접두사로 조절할 수 있어요:
{.emit: """/*TYPESECTION*/
struct Vector3 {
public:
Vector3(): x(5) {}
Vector3(float x_): x(x_) {}
float x;
};
""".}
type Vector3 {.importcpp: "Vector3", nodecl} = object
x: cfloat
proc constructVector3(a: cfloat): Vector3 {.importcpp: "Vector3(@)", nodecl}
ImportCpp 프라그마
참고: c2nim은 C++의 큰 부분집합을 파싱할 수 있고, importcpp 프라그마 패턴 언어를 알고 있어요. 여기 설명된 모든 세부 사항을 알 필요는 없습니다.
C를 위한 [importc 프라그마]와 비슷하게, importcpp 프라그마로 C++ 메서드나 일반적으로 C++ 심볼을 import할 수 있어요. 그러면 생성된 코드가 C++ 메서드 호출 문법, 즉 obj->method(arg)를 사용합니다. header와 emit 프라그마와 결합하면 C++로 작성된 라이브러리와 대충(sloppy) 인터페이스하는 걸 허용해요:
# C++ 엔진과 인터페이스하는 끔찍한 예시 ... ;-)
{.link: "/usr/lib/libIrrlicht.so".}
{.emit: """
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
""".}
const
irr = "<irrlicht/irrlicht.h>"
type
IrrlichtDeviceObj {.header: irr,
importcpp: "IrrlichtDevice".} = object
IrrlichtDevice = ptr IrrlichtDeviceObj
proc createDevice(): IrrlichtDevice {.
header: irr, importcpp: "createDevice(@)".}
proc run(device: IrrlichtDevice): bool {.
header: irr, importcpp: "#.run(@)".}
이게 동작하려면 컴파일러에게 C++을 생성하라고(명령 cpp) 알려야 해요. 컴파일러가 C++ 코드를 방출할 때 조건부 심볼 cpp가 정의됩니다.
네임스페이스 (Namespaces)
위의 대충 인터페이스하는 예시는 .emit으로 using namespace 선언을 만들었어요. 보통은 대신 namespace::identifier 표기로 import한 이름을 가리키는 게 훨씬 낫습니다:
type
IrrlichtDeviceObj {.header: irr,
importcpp: "irr::IrrlichtDevice".} = object
열거형에 대한 Importcpp (Importcpp for enums)
importcpp를 열거형 타입에 적용하면 수치 열거형 값이 C++ 열거형 타입으로 주석 처리돼요. 다음 예시처럼요: ((TheCppEnum)(3)). (이게 구현하기 가장 단순한 방법이었어요.)
프로시저에 대한 Importcpp (Importcpp for procs)
프로시저용 importcpp 변형은 최대한의 유연성을 위해 다소 난해한 패턴 언어를 사용한다는 점에 주의하세요:
- 해시
#심볼은 첫 번째 또는 다음 인자로 치환돼요. - 해시 뒤에 오는 점
#.은 호출이 C++의 점 또는 화살표 표기를 사용해야 함을 나타내요. - at 심볼
@은 남은 인자들로 치환되는데, 쉼표로 구분됩니다.
예를 들면:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "#.CppMethod(@)".}
var x: ptr CppObj
cppMethod(x[], 1, 2, 3)
다음을 생성해요:
x->CppMethod(1, 2, 3)
importcpp 프라그마의 구버전과의 하위 호환을 유지하기 위한 특별한 규칙으로, 특수 패턴 문자(# ' @ 중 어떤 것도)가 전혀 없으면 C++의 점 또는 화살표 표기가 가정돼요. 그래서 위 예시는 다음과 같이 쓸 수도 있어요:
proc cppMethod(this: CppObj, a, b, c: cint) {.importcpp: "CppMethod".}
패턴 언어가 자연스럽게 C++의 연산자 오버로딩 기능도 다룬다는 점을 참고하세요:
proc vectorAddition(a, b: Vec3): Vec3 {.importcpp: "# + #".}
proc dictLookup(a: Dict, k: Key): Value {.importcpp: "#[#]".}
- 아포스트로피
'뒤에 0..9 범위의 정수i가 오면 i번째 매개변수 타입으로 치환돼요. 0번 위치는 결과 타입입니다. 이걸로 C++ 함수 템플릿에 타입을 전달할 수 있어요.'과 숫자 사이에 별표를 쓰면 타입의 기본 타입(base type)을 얻을 수 있어요. (그래서 타입에서 "별 하나를 떼내는" 셈이죠;T*가T가 됩니다.) 별표 두 개를 쓰면 요소 타입의 요소 타입을 얻는 식입니다.
예를 들면:
type Input {.importcpp: "System::Input".} = object
proc getSubsystem*[T](): ptr T {.importcpp: "SystemManager::getSubsystem<'*0>()", nodecl.}
let x: ptr Input = getSubsystem[Input]()
다음을 생성해요:
x = SystemManager::getSubsystem<System::Input>()
#@은cnew연산을 지원하기 위한 특별한 경우예요. 호출 표현식이 임시 위치를 거치지 않고 직접 인라인되도록 하기 위해 필요합니다. 이건 현재 코드 생성기의 한계를 우회하기 위해서만 필요해요.
예를 들어 C++의 new 연산자를 다음과 같이 "import"할 수 있어요:
proc cnew*[T](x: T): ptr T {.importcpp: "(new '*0#@)", nodecl.}
# 'Foo'의 생성자:
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)".}
let x = cnew constructFoo(3, 4)
다음을 생성해요:
x = new Foo(3, 4)
그런데 용도에 따라 new Foo는 대신 이렇게 래핑할 수도 있어요:
proc newFoo(a, b: cint): ptr Foo {.importcpp: "new Foo(@)".}
let x = newFoo(3, 4)
생성자 래핑 (Wrapping constructors)
때로 C++ 클래스에 private 복사 생성자가 있어서 Class c = Class(1,2); 같은 코드를 생성하면 안 되고 대신 Class c(1,2);를 생성해야 할 때가 있어요. 이를 위해 C++ 생성자를 래핑하는 Nim 프로시저에 constructor 프라그마를 붙여야 합니다. 이 프라그마는 생성 시 복사 생성자를 호출하지 않으므로 더 빠른 C++ 코드를 만드는 데도 도움이 돼요:
# 'Foo'의 더 나은 생성자:
proc constructFoo(a, b: cint): Foo {.importcpp: "Foo(@)", constructor.}
소멸자 래핑 (Wrapping destructors)
Nim이 C++을 직접 생성하므로, 어떤 소멸자든 스코프를 빠져나갈 때 C++ 컴파일러가 암묵적으로 호출해요. 즉 소멸자 자체를 전혀 래핑하지 않고 처리할 수 있는 경우가 많습니다! 하지만 명시적으로 호출해야 할 때는 래핑이 필요해요. 패턴 언어가 필요한 모든 것을 제공합니다:
proc destroyFoo(this: var Foo) {.importcpp: "#.~Foo()".}
객체에 대한 Importcpp (Importcpp for objects)
일반적인 importcpp 객체는 C++ 템플릿에 매핑돼요. 그러면 객체 타입에 패턴 언어가 필요 없이 C++ 템플릿을 아주 쉽게 import할 수 있다는 뜻입니다:
type
StdMap[K, V] {.importcpp: "std::map", header: "<map>".} = object
proc `[]=`[K, V](this: var StdMap[K, V]; key: K; val: V) {.
importcpp: "#[#] = #", header: "<map>".}
var x: StdMap[cint, cdouble]
x[6] = 91.4
다음을 생성해요:
std::map<int, double> x;
x[6] = 91.4;
- 더 세밀한 제어가 필요하면, 제공된 패턴에서 아포스트로피
'를 사용해서 제네릭 타입의 구체적인 타입 매개변수를 나타낼 수 있어요. 자세한 내용은 프로시저 패턴에서 아포스트로피 연산자의 사용을 보세요.
type
VectorIterator[T] {.importcpp: "std::vector<'0>::iterator".} = object
var x: VectorIterator[cint]
다음을 생성해요:
std::vector<int>::iterator x;
ImportJs 프라그마
C++를 위한 [importcpp 프라그마]와 비슷하게, importjs 프라그마로 Javascript 메서드나 일반적으로 심볼을 import할 수 있어요. 그러면 생성된 코드가 Javascript 메서드 호출 문법, 즉 obj.method(arg)를 사용합니다.
ImportObjC 프라그마
C를 위한 [importc 프라그마]와 비슷하게, importobjc 프라그마로 Objective C 메서드를 import할 수 있어요. 그러면 생성된 코드가 Objective C 메서드 호출 문법, 즉 [obj method param1: arg]를 사용합니다. 게다가 header와 emit 프라그마와 결합하면 Objective C로 작성된 라이브러리와 대충 인터페이스하는 걸 허용해요:
# GNUStep과 인터페이스하는 끔찍한 예시 ...
{.passl: "-lobjc".}
{.emit: """
#include <objc/Object.h>
@interface Greeter:Object
{
}
- (void)greet:(long)x y:(long)dummy;
@end
#include <stdio.h>
@implementation Greeter
- (void)greet:(long)x y:(long)dummy
{
printf("Hello, World!\n");
}
@end
#include <stdlib.h>
""".}
type
Id {.importc: "id", header: "<objc/Object.h>", final.} = distinct int
proc newGreeter: Id {.importobjc: "Greeter new", nodecl.}
proc greet(self: Id, x, y: int) {.importobjc: "greet", nodecl.}
proc free(self: Id) {.importobjc: "free", nodecl.}
var g = newGreeter()
g.greet(12, 34)
g.free()
이게 동작하려면 컴파일러에게 Objective C를 생성하라고(명령 objc) 알려야 해요. 컴파일러가 Objective C 코드를 방출할 때 조건부 심볼 objc가 정의됩니다.
CodegenDecl 프라그마
codegenDecl 프라그마로 Nim의 코드 생성기에 직접 영향을 줄 수 있어요. 변수, 프로시저 또는 객체 타입이 생성 코드에서 어떻게 선언될지를 결정하는 포맷 문자열을 받습니다.
변수의 경우, 포맷 문자열에서 $1은 변수의 타입, $2는 변수의 이름을 나타내고, $#이 나타날 때마다 그 위치에 따라 각각 $1/$2로 치환돼요.
다음 Nim 코드는:
var
a {.codegenDecl: "$# progmem $#".}: int
이 C 코드를 생성합니다:
int progmem a
프로시저의 경우 $1은 프로시저의 반환 타입, $2는 프로시저의 이름, $3은 매개변수 목록을 나타내고, $#이 나타날 때마다 그 위치에 따라 각각 $1/$2/$3으로 치환돼요.
다음 Nim 코드는:
proc myinterrupt() {.codegenDecl: "__interrupt $# $#$#".} =
echo "realistic interrupt handler"
이 코드를 생성합니다:
__interrupt void myinterrupt()
객체 타입의 경우 $1은 객체 타입의 이름, $2는 필드 목록, $3은 기본 타입을 나타내요.
const strTemplate = """
struct $1 {
$2
};
"""
type Foo {.codegenDecl:strTemplate.} = object
a, b: int
이 코드를 생성합니다:
struct Foo {
NI a;
NI b;
};
cppNonPod 프라그마
cppNonPod 프라그마는 non-POD importcpp 타입에 써야 해요. 그래야 특히 생성자와 소멸자에 관해 threadvar 변수에서 제대로 동작합니다. 이것은 --tlsEmulation:off가 필요해요.
type Foo {.cppNonPod, importcpp, header: "funs.h".} = object
x: cint
proc main()=
var a {.threadvar.}: Foo
컴파일 타임 define 프라그마
여기 나열된 프라그마들은 컴파일 타임에 -d/--define 옵션으로 값을 선택적으로 받는 데 쓸 수 있어요.
현재 구현은 다음 옵션들을 제공합니다(다른 것들은 나중에 추가될 수 있어요).
| 프라그마 | 설명 |
|---|---|
intdefine |
빌드 타임 define을 정수로 읽음 |
strdefine |
빌드 타임 define을 문자열로 읽음 |
booldefine |
빌드 타임 define을 bool로 읽음 |
const FooBar {.intdefine.}: int = 5
echo FooBar
nim c -d:FooBar=42 foobar.nim
위 예시에서 -d 플래그를 주면 심볼 FooBar가 컴파일 타임에 덮어써져서 42가 출력돼요. -d:FooBar=42를 생략하면 기본값 5가 사용됩니다. 값이 제공됐는지 확인하려면 defined(FooBar)를 쓰면 돼요.
-d:flag 문법은 실제로 -d:flag=true의 단축형일 뿐이에요.
이 프라그마들은 자격을 갖춘(qualified) define 이름을 위한 선택적 문자열 인자도 받아요.
const FooBar {.intdefine: "package.FooBar".}: int = 5
echo FooBar
nim c -d:package.FooBar=42 foobar.nim
이렇게 하면 서로 다른 패키지에서 define 이름이 헷갈리지 않게 구분하는 데 도움이 돼요.
상수 값에 기반해 define의 타입을 감지하는 이 프라그마들의 버전은 일반적인 define 프라그마를 보세요.