사용자 정의 프라그마

사용자 정의 프라그마 (User-defined pragmas)

pragma 프라그마 (pragma pragma)

pragma 프라그마를 사용하면 사용자 정의 프라그마(user-defined pragma)를 선언할 수 있어요. 이것이 유용한 이유는 Nim의 템플릿과 매크로는 프라그마에 영향을 미치지 못하기 때문이에요. 사용자 정의 프라그마는 다른 모든 심볼과는 별개의 모듈 전체(module-wide) 스코프에 존재해요. 그래서 모듈에서 가져오기(import)할 수도 없어요.

예시를 볼게요:

when appType == "lib":
  {.pragma: rtl, exportc, dynlib, cdecl.}
else:
  {.pragma: rtl, importc, dynlib: "client.dll", cdecl.}

proc p*(a, b: int): int {.rtl.} =
  result = a + b

이 예시에서는 rtl이라는 새 프라그마를 도입했어요. 이 프라그마는 동적 라이브러리(dynamic library)에서 심볼을 가져오거나(import), 동적 라이브러리 생성을 위해 심볼을 내보내는(export) 역할을 해요.

커스텀 애너테이션 (Custom annotations)

커스텀 타입이 지정된(typed) 프라그마를 정의할 수도 있어요. 커스텀 프라그마는 코드 생성에 직접 영향을 미치지는 않지만, 그 존재를 매크로로 감지할 수 있어요. 커스텀 프라그마는 pragma 프라그마로 애너테이션된 템플릿을 사용해 정의해요:

template dbTable(name: string, table_space: string = "") {.pragma.}
template dbKey(name: string = "", primary_key: bool = false) {.pragma.}
template dbForeignKey(t: typedesc) {.pragma.}
template dbIgnore {.pragma.}

가능한 객체 관계 매핑(ORM, Object Relation Mapping) 구현의 정형화된(stylized) 예시를 살펴볼게요:

const tblspace {.strdefine.} = "dev" # dev, test, prod 환경을 전환하는 스위치

type
  User {.dbTable("users", tblspace).} = object
    id {.dbKey(primary_key = true).}: int
    name {.dbKey"full_name".}: string
    is_cached {.dbIgnore.}: bool
    age: int

  UserProfile {.dbTable("profiles", tblspace).} = object
    id {.dbKey(primary_key = true).}: int
    user_id {.dbForeignKey: User.}: int
    read_access: bool
    write_access: bool
    admin_access: bool

이 예시에서는 커스텀 프라그마를 사용해 Nim 객체가 관계형 데이터베이스(relational database)의 스키마에 어떻게 매핑되는지 표현했어요. 커스텀 프라그마는 인자를 0개 이상 가질 수 있어요. 여러 인자를 넘기려면 템플릿 호출 문법 중 하나를 사용하면 돼요. 모든 인자는 타입이 지정되어 있고, 표준 오버로딩 해석 규칙을 따르기 때문에 인자의 기본값, 이름으로 전달, varargs 등을 지원해요.

커스텀 프라그마는 일반 프라그마를 지정할 수 있는 모든 위치에서 사용할 수 있어요. 프로시저, 템플릿, 타입·변수 정의, 문(statement) 등에 애너테이션을 달 수 있어요.

macros 모듈에는 커스텀 프라그마 접근을 단순화하는 헬퍼인 hasCustomPragma, getCustomPragmaVal이 포함되어 있어요. 자세한 내용은 macros 모듈 문서를 참고하세요. 이 매크로들은 특별한 게 아니에요. 이들이 하는 모든 일은 객체 표현의 AST를 순회(walking)하는 것으로도 달성할 수 있어요.

커스텀 프라그마를 사용한 예시를 더 볼게요:

  • 더 나은 직렬화/역직렬화 제어:
type MyObj = object
    a {.dontSerialize.}: int
    b {.defaultDeserialize: 5.}: int
    c {.serializationKey: "_c".}: string
  • 게임 엔진에서 GUI 인스펙터(inspector)를 위한 타입 채택:
type MyComponent = object
    position {.editable, animatable.}: Vector3
    alpha {.editRange: [0.0..1.0], animatable.}: float32

매크로 프라그마 (Macro pragmas)

매크로와 템플릿은 가끔 프라그마 문법으로 호출할 수도 있어요. 이것이 가능한 경우에는 루틴(프로시저, 이터레이터 등) 선언이나 루틴 타입 표현식에 붙이는 경우가 포함돼요. 컴파일러는 다음과 같은 단순한 구문 변환(syntactic transformation)을 수행해요:

template command(name: string, def: untyped) = discard

proc p() {.command("print").} = discard

이것은 다음과 같이 변환돼요:

command("print"):
  proc p() = discard

type
  AsyncEventHandler = proc (x: Event) {.async.}

이것은 다음과 같이 변환돼요:

type
  AsyncEventHandler = async(proc (x: Event))

같은 정의에 여러 매크로 프라그마가 적용되면, 왼쪽에서 오른쪽으로 첫 번째 것부터 평가돼요. 이 매크로는 자신의 출력에 나머지 매크로 프라그마를 남겨둘 수 있고, 그 프라그마들도 같은 방식으로 평가돼요.

매크로 프라그마에는 타입, 변수, 상수 선언에서 쓰이는 것 같은 몇 가지 더 많은 응용이 있지만, 이 동작은 실험적(experimental)으로 간주되며 실험 매뉴얼에 대신 문서화되어 있어요.