외부 함수 인터페이스
외부 함수 인터페이스 (Foreign function interface)
Nim의 FFI:idx: (외부 함수 인터페이스, foreign function interface)는 광범위하고, 여기서는 미래의 다른 백엔드(LLVM/JavaScript 백엔드 같은)로 확장 가능한 부분만 다룰게요.
Importc 프라그마 (Importc pragma)
importc 프라그마는 프로시저나 변수를 C에서 가져오기(import) 위한 수단이에요. 선택적 인자는 C 식별자를 담은 문자열이에요. 인자가 없으면 C 이름은 Nim 식별자를 정확히 그대로 써요:
proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}
importc를 let 문에 적용하면 그 값을 생략할 수 있는데, 그 값은 C에서 온다고 기대해요. 이걸로 C의 const:c: 를 가져올 수 있어요:
{.emit: "const int cconst = 42;".}
let cconst {.importc, nodecl.}: cint
assert cconst == 42
이 프라그마는 과거에 JS 백엔드에서 JS 객체와 함수를 위해 남용된 적이 있어요. 다른 백엔드도 같은 이름으로 같은 기능을 제공해요. 또한 타깃 언어가 C가 아니면 다른 프라그마를 쓸 수 있어요:
- [importcpp][importcpp pragma]
- [importobjc][importobjc pragma]
- [importjs][importjs pragma]
importc에 전달되는 문자열 리터럴은 포맷 문자열(format string)일 수 있어요:
proc p(s: cstring) {.importc: "prefix$1".}
예시에서 p의 외부 이름은 prefixp로 설정돼요. $1만 쓸 수 있고, 문자 그대로의 달러 기호는 $$로 적어야 해요.
Exportc 프라그마 (Exportc pragma)
exportc 프라그마는 타입, 변수 또는 프로시저를 C로 내보내기(export) 위한 수단이에요. 열거형(enum)과 상수(const)는 내보낼 수 없어요. 선택적 인자는 C 식별자를 담은 문자열이에요. 인자가 없으면 C 이름은 Nim 식별자를 정확히 그대로 써요:
proc callme(formatstr: cstring) {.exportc: "callMe", varargs.}
이 프라그마는 다소 잘못된 이름이라는 점에 주의하세요: 다른 백엔드도 같은 이름으로 같은 기능을 제공해요.
exportc에 전달되는 문자열 리터럴은 포맷 문자열일 수 있어요:
proc p(s: string) {.exportc: "prefix$1".} =
echo s
예시에서 p의 외부 이름은 prefixp로 설정돼요. $1만 쓸 수 있고, 문자 그대로의 달러 기호는 $$로 적어야 해요.
심볼을 동적 라이브러리로도 내보내야 한다면, exportc 프라그마에 더해 dynlib 프라그마를 사용해야 해요. Dynlib 프라그마 - export를 참고하세요.
Exportcpp 프라그마 (Exportcpp pragma)
exportcpp 프라그마는 exportc 프라그마처럼 동작하지만 cpp 백엔드가 필요해요. cpp 백엔드로 컴파일하면 exportc 프라그마가 생성된 코드의 선언에 export "C"를 추가해서 C 코드와 C++ 코드 양쪽에서 호출할 수 있게 해줘요. exportcpp 프라그마는 export "C"를 추가하지 않아요.
Extern 프라그마 (Extern pragma)
exportc나 importc처럼, extern 프라그마는 이름 맹글링(name mangling)에 영향을 줘요. extern에 전달되는 문자열 리터럴은 포맷 문자열일 수 있어요:
proc p(s: string) {.extern: "prefix$1".} =
echo s
예시에서 p의 외부 이름은 prefixp로 설정돼요. $1만 쓸 수 있고, 문자 그대로의 달러 기호는 $$로 적어야 해요.
Bycopy 프라그마 (Bycopy pragma)
bycopy 프라그마는 객체(object)나 튜플(tuple) 타입 또는 프로시저 매개변수에 적용할 수 있어요. 컴파일러에게 그 타입을 프로시저에 값으로(by value) 전달하라고 지시해요:
type
Vector {.bycopy.} = object
x, y, z: float
Nim 컴파일러는 매개변수 타입의 크기에 따라 매개변수를 값으로 전달할지 참조로 전달할지 자동으로 결정해요. 매개변수를 값으로 또는 참조로 반드시 전달해야 한다면(C 라이브러리와 인터페이스할 때처럼) bycopy나 byref 프라그마를 쓰세요. byref로 표시된 매개변수가 bycopy로 표시된 타입보다 우선한다는 점을 유의하세요.
Byref 프라그마 (Byref pragma)
byref 프라그마는 객체나 튜플 타입 또는 프로시저 매개변수에 적용할 수 있어요. 타입에 적용하면 컴파일러에게 그 타입을 프로시저에 참조로(숨겨진 포인터, hidden pointer) 전달하라고 지시해요. 매개변수에 적용하면 타입이 bycopy로 표시되었더라도 그것이 우선해요. importc 타입에 byref 프라그마가 있거나 importc 프로시저의 매개변수가 byref로 표시되면, 그 매개변수들은 포인터로 변환돼요. importcpp 타입에 byref 프라그마가 있으면 이 매개변수들은 C++ 참조 &로 변환돼요.
{.emit: """/*TYPESECTION*/
typedef struct {
int x;
} CStruct;
""".}
{.emit: """
#ifdef __cplusplus
extern "C"
#endif
int takesCStruct(CStruct* x) {
return x->x;
}
""".}
type
CStruct {.importc, byref.} = object
x: cint
proc takesCStruct(x: CStruct): cint {.importc.}
또는
type
CStruct {.importc.} = object
x: cint
proc takesCStruct(x {.byref.}: CStruct): cint {.importc.}
{.emit: """/*TYPESECTION*/
struct CppStruct {
int x;
int takesCppStruct(CppStruct& y) {
return x + y.x;
}
};
""".}
type
CppStruct {.importcpp, byref.} = object
x: cint
proc takesCppStruct(x, y: CppStruct): cint {.importcpp.}
Varargs 프라그마 (Varargs pragma)
varargs 프라그마는 프로시저에만(그리고 프로시저 타입에) 적용할 수 있어요. Nim에게 그 프로시저가 마지막으로 지정된 매개변수 뒤에 가변 개수의 매개변수를 받을 수 있다고 알려줘요. Nim 문자열 값은 C 문자열로 자동 변환돼요:
proc printf(formatstr: cstring) {.header: "<stdio.h>", varargs.}
printf("hallo %s", "world") # "world"는 C 문자열로 전달됨
Union 프라그마 (Union pragma)
union 프라그마는 어떤 object 타입에도 적용할 수 있어요. 객체의 모든 필드가 메모리에서 겹쳐진다(overlaid)는 뜻이에요. 그러면 생성된 C/C++ 코드에서 struct:c: 대신 union:c: 가 만들어져요. 객체 선언은 그때 상속(inheritance)이나 GC가 관리하는(GC'ed) 메모리를 쓰면 안 되지만, 현재는 이것이 검사되지 않아요.
미래 방향(Future directions): GC가 관리하는 메모리가 union에서 허용되어야 하고, GC는 union을 보수적으로(conservatively) 스캔해야 해요.
Packed 프라그마 (Packed pragma)
packed 프라그마는 어떤 object 타입에도 적용할 수 있어요. 객체의 필드가 메모리에서 연속으로 붙어(packed back-to-back) 배치되도록 보장해요. 네트워크나 하드웨어 드라이버에서 주고받는 패킷이나 메시지를 저장할 때, 그리고 C와의 상호 운용(interoperability)에 유용해요. packed 프라그마와 상속을 결합하는 것은 정의되어 있지 않고, GC가 관리하는 메모리(ref)와 함께 쓰면 안 돼요.
미래 방향(Future directions): packed 프라그마에서 GC가 관리하는 메모리를 쓰면 정적 오류(static error)가 될 거예요. 상속과의 조합은 정의되고 문서화되어야 해요.
Dynlib 프라그마 - import (Dynlib pragma for import)
dynlib 프라그마를 쓰면 프로시저나 변수를 동적 라이브러리에서 가져올 수 있어요(Windows의 .dll 파일, UNIX의 lib*.so 파일). 필수 인자는 동적 라이브러리의 이름이어야 해요:
proc gtk_image_new(): PGtkWidget
{.cdecl, dynlib: "libgtk-x11-2.0.so", importc.}
일반적으로 동적 라이브러리를 가져오는 데는 특별한 링커 옵션이나 임포트 라이브러리와의 링킹이 필요 없어요. 즉 devel 패키지를 설치할 필요도 없다는 뜻이에요.
dynlib 가져오기 메커니즘은 버전 관리(versioning) 방식을 지원해요:
proc Tcl_Eval(interp: pTcl_Interp, script: cstring): int {.cdecl,
importc, dynlib: "libtcl(|8.5|8.4|8.3).so.(1|0)".}
런타임에 동적 라이브러리는 다음 순서로 검색돼요:
libtcl.so.1
libtcl.so.0
libtcl8.5.so.1
libtcl8.5.so.0
libtcl8.4.so.1
libtcl8.4.so.0
libtcl8.3.so.1
libtcl8.3.so.0
dynlib 프라그마는 인자로 상수 문자열뿐 아니라 일반적인 문자열 표현식도 지원해요:
import std/os
proc getDllName: string =
result = "mylib.dll"
if fileExists(result): return
result = "mylib2.dll"
if fileExists(result): return
quit("could not load dynamic library")
proc myImport(s: cstring) {.cdecl, importc, dynlib: getDllName().}
참고:
libtcl(|8.5|8.4).so같은 패턴은 미리 컴파일(precompile)되기 때문에 상수 문자열에서만 지원돼요.
참고:
dynlib프라그마에 변수를 전달하면 초기화 순서 문제 때문에 런타임에 실패해요.
참고:
dynlib가져오기는--dynlibOverride:name:option: 명령줄 옵션으로 덮어쓸 수 있어요. 컴파일러 사용자 가이드에 더 자세한 정보가 있어요.
Dynlib 프라그마 - export (Dynlib pragma for export)
dynlib 프라그마를 쓰면 프로시저를 동적 라이브러리로 내보낼 수도 있어요. 이 프라그마는 인자가 없고 exportc 프라그마와 함께 써야 해요:
proc exportme(): int {.cdecl, exportc, dynlib.}
이것은 프로그램이 --app:lib:option: 명령줄 옵션으로 동적 라이브러리로 컴파일될 때만 유용해요.