參考資訊

如何呼叫 C Function

需配合 pragma 告訴 Nim compiler 特定的 proc 其實是從 C 來的, 例如

proc exit(status: cint) {.importc.}

exit(0)

這是最簡單的型式

  • importc 告知 compiler exit() 是個 C function
  • status: cint 因為 C 的 int 是不定長的, 所以 C 參數 int status, 需要配合 nim 提供的 cint type 做為對應
  • 這裡沒有提供 header 資訊, 但 nim 會為我們產生相對應的 C prototype, 所以 compiler 時不會有 warning 的

呼叫帶參數的 C Function

type
    FILE {.importc, header: "<stdio.h>".} = object

proc fopen(pathname: cstring, mode: cstring): ptr FILE {.importc, header: "<stdio.h>".}
proc fclose(f: ptr FILE) {.importc, header: "<stdio.h>".}

let f = fopen("/etc/passwd", "r")
fclose(f)
  • FILE 在此我們宣告它是來自 C 的 type (importc), 對應為 nim 的 object, 具體的宣告來自於 stdio.h, 所以其實細節我們在 binding 並沒有過多的說明, 就用 object 帶過. 在碰到 opaque type 時基本上都能這麼處理 (不需要 member 的描述)
  • fopen() 在 C 裡傳回的類型是 FILE *, 我們在 nim 裡用 ptr FILE 對應
  • header 後的參數如果不加上 <>, 那產生出來的會是
    #include "stdio.h"
    加上的話, 則為
    #include <stdio.h>

變更 Import 進來的 Name

importc 可以給一個參數, 跟 nim compiler 說 import 進來的東西在 C 裡的長像, 除了可以用做變更在 nim 中的 type or proc name, 也可以像下面這樣添加些細節

type
    FILE {.importc: "FILE *", header: "<stdio.h>".} = ptr object

proc fopen(pathname: cstring, mode: cstring): FILE {.importc, header: "<stdio.h>".}
proc fclose(f: FILE) {.importc, header: "<stdio.h>".}

let f = fopen("/etc/passwd", "r")
fclose(f)

簡化 Binding 描述

以前例而言, 我們重複了多次 {.importc, header: "<stdio.h>".}, nim 提供了 {.push}{.pop} 用來去掉有相同描述的區塊中重複的部份

{.push, importc, header: "<stdio.h>".}
type
    FILE = object

proc fopen(pathname: cstring, mode: cstring): ptr FILE
proc fclose(f: ptr FILE)
{.pop}

let f = fopen("/etc/passwd", "r")
fclose(f)

上面的例孑是呼叫 C library 中的 function, 所以不需要額外指定 linker 參數, 但如果要跟其他 library link 在一塊, 就要告知 linker 參數, 例如

{.push, importc, header: "<systemd/sd-event.h>".}
type
    sd_event = object

proc sd_event_new(e: ptr ptr sd_event): cint
proc sd_event_unref(e: ptr sd_event): ptr sd_event
{.pop}

var e: ptr sd_event
if sd_event_new(addr e) < 0:
    echo "error"
else:
    e = sd_event_unref(e)

這個例子在 link time 會失敗, 因為所需的 symbol 沒有缺失 (在 libsystemd.so 裡, 但我們沒跟它 link)

$ nim c -r test.nim
Hint: used config file '/etc/nim/nim.cfg' [Conf]
Hint: system [Processing]
Hint: widestrs [Processing]
Hint: io [Processing]
Hint: test [Processing]
CC: stdlib_io.nim
CC: stdlib_system.nim
CC: test.nim
Hint:  [Link]
/usr/bin/ld: /home/derekdai/.cache/nim/test_d/@mtest.nim.c.o: in function `NimMainModule':
@mtest.nim.c:(.text+0x21f): undefined reference to `sd_event_new'
/usr/bin/ld: @mtest.nim.c:(.text+0x275): undefined reference to `sd_event_unref'
collect2: error: ld returned 1 exit status
Error: execution of an external program failed: 'gcc   -o /home/derekdai/test-nim/test  /home/derekdai/.cache/nim/test_d/stdlib_io.nim.c.o /home/derekdai/.cache/nim/test_d/stdlib_system.nim.c.o /home/derekdai/.cache/nim/test_d/@mtest.nim.c.o    -ldl'

需要加上 passL pragma 提供 linker 參數

{.passL: "-lsystemd"}
{.push, importc, header: "<systemd/sd-event.h>".}
type
    sd_event = object

proc sd_event_new(e: ptr ptr sd_event): cint
proc sd_event_unref(e: ptr sd_event): ptr sd_event
{.pop}

var e: ptr sd_event
if sd_event_new(addr e) < 0:
    echo "error"
else:
    e = sd_event_unref(e)

補充二點

  • 因為 sd_event_new(sd_event **e) 是經由參數傳回新建立的 sd_event *, 所以上面宣告時, 參數是 ptr ptr sd_event, 對應到 C 的 sd_event **
  • 要取回 sd_event_new() 的回傳值, 我們宣告了
    var e: ptr sd_event
    並且用 addr 取得 e 的 address 傳進 sd_event_new()
    sd_event_new(addr e)

配合 pkg-config 取得 Library 資訊

上例我們直接把 -lsystemd 參數以 passL 傳給了 link, nim 還能配合 pkg-config 取得這些資訊. nim 的 system module 有個 proc gorge(), 能在 compile time 執行 command, 並將 stdout 回傳給 nim compiler 當 const 使用

{.passL: "-lsystemd"}
{.push, importc, header: "<systemd/sd-event.h>".}
type
    sd_event = object

proc sd_event_new(e: ptr ptr sd_event): cint
proc sd_event_unref(e: ptr sd_event): ptr sd_event
{.pop}

var e: ptr sd_event
if sd_event_new(addr e) < 0:
    echo "error"
else:
    e = sd_event_unref(e)

呼叫 Variaic Function

printf() 為例, prototype 為

extern int printf (const char *__restrict __format, ...);

相對應的 nim 宣告如下

proc printf(format: cstring) {.importc, varargs.}

let v: int = 0
printf("Hello World, %p\n", unsafeAddr v)

這裡取得 v 的 address 需要用到 unsafeAddr, 具體跟 addr 的使用時機差異目前還末瞭解.

以上例來說, 產生的 C code 會帶有自己的 prototype

...
  N_NIMCALL(void, printf)(NCSTRING format, ...);
...
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
        nimfr_("test", "/home/derekdai/test-nim/test.nim");
        nimln_(4, "/home/derekdai/test-nim/test.nim");
        printf("Hello World, %p\012", (&v__68UvaOgL9c7n49a3f9blODEiQ));
        popFrame();
}
}

雖說這麼寫 nim 在 compiler 的過程也沒有什麼 warning 產生, 但可以看到其實 C 的 prototype 提供了更多的資訊及限制以供 compiler 進行檢查, 所以最好還是提供 header 給 nim, 而不是讓它自行產生相對應的 prototype

proc printf(format: cstring) {.importc, varargs, header: "<stdio.h>".}

let v: int = 0
printf("Hello World, %p\n", unsafeAddr v)

產生的 C code

...
#include "stdio.h"
...
N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
        nimfr_("test", "/home/derekdai/test-nim/test.nim");
        nimln_(4, "/home/derekdai/test-nim/test.nim");
        printf("Hello World, %p\012", (&v__68UvaOgL9c7n49a3f9blODEiQ));
        popFrame();
}
}

查看產生的 C code

位置在 ~/.cache/nim/

運行時期動態載入 Library

nim 除了提供 passL 在 link time 跟指定的 library link 在一塊之外的另一個選項 - dynlib. dynlib 是配合了 dlopen(), dlclose(), dlsym() 在 runtime 載入 shared library 並取得 symbol 的 address 進行操作, 所以通過這種方式使用的 nim 程式用 ldd 不會看到跟目標 shared libarary 的 link 資訊.

另外, dynlib 因為不需要 header, 所以像是 libsystemd-dev 這樣的 package 也變得不需要.

另外, dynlibheader 是互斥的, 不可同時使用. 我們來改寫上面 systemd 的例子

{.push, importc, dynlib: "libsystemd.so".}
type
    sd_event = object

proc sd_event_new(e: ptr ptr sd_event): cint
proc sd_event_unref(e: ptr sd_event): ptr sd_event
{.pop}

var e: ptr sd_event
if sd_event_new(addr e) < 0:
    echo "error"
else:
    e = sd_event_unref(e)

Build 出的 object file 看不出跟 libsystemd.so 的關係

        linux-vdso.so.1 (0x00007ffd005ef000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9d942f9000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9d94107000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9d94344000)

相較之下, 使用 passL 則有清楚的關係

        linux-vdso.so.1 (0x00007ffca3ec9000)
        libsystemd.so.0 => /lib/x86_64-linux-gnu/libsystemd.so.0 (0x00007f7b353e9000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7b351f7000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f7b351ec000)
        liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007f7b351c5000)
        liblz4.so.1 => /lib/x86_64-linux-gnu/liblz4.so.1 (0x00007f7b351a4000)
        libgcrypt.so.20 => /lib/x86_64-linux-gnu/libgcrypt.so.20 (0x00007f7b35086000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7b35061000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f7b354db000)
        libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007f7b3503e000)

另外, 產生的 C code 載入 shared library 的程式片段如下

N_LIB_PRIVATE N_NIMCALL(void, testDatInit000)(void) {
if (!((TM__ipcYmBC9bj9a1BW35ABoB1Kw_2 = nimLoadLibrary(((NimStringDesc*) &TM__ipcYmBC9bj9a1BW35ABoB1Kw_4)))
)) nimLoadLibraryError(((NimStringDesc*) &TM__ipcYmBC9bj9a1BW35ABoB1Kw_5));
        Dl_3382005_ = (tyProc__TbO9aYDtp9aP82rTjwF9aAnWg) nimGetProcAddr(TM__ipcYmBC9bj9a1BW35ABoB1Kw_2, "sd_event_new");
        Dl_3382010_ = (tyProc__4vNa8z4Q8PM8Ve7tgtHhBg) nimGetProcAddr(TM__ipcYmBC9bj9a1BW35ABoB1Kw_2, "sd_event_unref");
}

可參考這裡.

合併 .nim 成為一個 module

使用 include 能把多個 .nim 合併成為一個 module - 不管是對 nim compiler 而言, 或是 module 的使用者而言都是 (功能其實跟 C 的 #include 一樣).

以 sdl2_nim project 例子, 它將 sdl2/private/*nim 集合起來成為 sdl2/sdl.nim 這一個 module

{.deadCodeElim: on.}
include
  private/sdl_libname,
  private/sdl_init,
  private/atomics,
  private/rwops,
  ...
  private/version

Bitfield, Enum, Flags

原文

type
  MyFlag* {.size: sizeof(cint).} = enum
    A
    B
    C
    D
  MyFlags = set[MyFlag]

proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)

assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}

sdl2_nim 中的, sdl2/private/sd_init.nim 並沒有用這個特性

#   INIT_*
#
#  These are the flags which may be passed to SDL_Init().  You should
#  specify the subsystems which you will be using in your application.
const
  INIT_TIMER* = 0x00000001
  INIT_AUDIO* = 0x00000010
  INIT_VIDEO* = 0x00000020  # implies INIT_EVENTS
  INIT_JOYSTICK* = 0x00000200 # implies INIT_EVENTS
  INIT_HAPTIC* = 0x00001000
  INIT_GAMECONTROLLER* = 0x00002000 # implies INIT_JOYSTICK
  INIT_EVENTS* = 0x00004000
  INIT_SENSOR* = 0x00008000
  INIT_NOPARACHUTE* = 0x00100000 # compatibility; this flag is ignored
  INIT_EVERYTHING* = (INIT_TIMER or INIT_AUDIO or INIT_VIDEO or INIT_EVENTS or
      INIT_JOYSTICK or INIT_HAPTIC or INIT_GAMECONTROLLER or INIT_SENSOR)

不過, sdl2/private/mouse.nim 中用到了 enum

type
  Cursor* = pointer ##  Implementation dependent

  SystemCursor* {.size: sizeof(cint).} = enum ##  \
    ##  Cursor types for ``sdl.createSystemCursor()``.
    SYSTEM_CURSOR_ARROW,      ##  Arrow
    SYSTEM_CURSOR_IBEAM,      ##  I-beam
    SYSTEM_CURSOR_WAIT,       ##  Wait
    SYSTEM_CURSOR_CROSSHAIR,  ##  Crosshair
    SYSTEM_CURSOR_WAITARROW,  ##  Small wait cursor (or Wait if not available)
    SYSTEM_CURSOR_SIZENWSE,   ##  Double arrow pointing northwest and southeast
    SYSTEM_CURSOR_SIZENESW,   ##  Double arrow pointing northeast and southwest
    SYSTEM_CURSOR_SIZEWE,     ##  Double arrow pointing west and east
    SYSTEM_CURSOR_SIZENS,     ##  Double arrow pointing north and south
    SYSTEM_CURSOR_SIZEALL,    ##  \
      ##  Four pointed arrow pointing north, south, east, and west
    SYSTEM_CURSOR_NO,         ##  Slashed circle or crossbones
    SYSTEM_CURSOR_HAND,       ##  Hand
    NUM_SYSTEM_CURSORS

Bitwise Or 運算

const
  INIT_EVERYTHING* = (INIT_TIMER or INIT_AUDIO or INIT_VIDEO or INIT_EVENTS or
      INIT_JOYSTICK or INIT_HAPTIC or INIT_GAMECONTROLLER or INIT_SENSOR)

一些亂七八糟的 Pointer 資訊

  • nil is also the default value for all ref and ptr types.
  • A dereferencing operation p[] implies that p is not nil
    p[] 這樣的方式 dereference
  • 配合
    {.experimental: "implicitDeref".}
    能簡化 dereference, 就可以直接寫成 p.data 而不是 p[].data
proc getMouseFocus*(): ptr Window {.
    cdecl, importc: "SDL_GetMouseFocus", dynlib: SDL2_LIB.}
  ##  Get the window which currently has mouse focus.

另一個例子

type
  AudioCallback* = proc (userdata: pointer; stream: ptr uint8; len: cint) {.
      cdecl.} ##  \
    ##  This procedure is called when the audio device needs more data.
    ##
    ##  ``userdata`` An application-specific parameter
    ##  saved in ``AudioSpec`` object.
    ##
    ##  ``stream`` A pointer to the audio data buffer.
    ##
    ##  ``len`` The length of that buffer in bytes.
    ##
    ##  Once the callback returns, the buffer will no longer be valid.
    ##  Stereo samples are stored in a LRLRLR ordering.
    ##
    ##  You can choose to avoid callbacks and use ``queueAudio()`` instead,
    ##  if you like. Just open your audio device with a `nil` callback.

通過 new 配置記憶體並存取

type
  Node = ref NodeObj
  NodeObj = object
    le, ri: Node
    data: int

var
  n: Node
new(n)
n.data = 9
# no need to write n[].data; in fact n[].data is highly discouraged!

Allocate, cast, pointer to and free

type
  Data = tuple[x, y: int, s: string]

# allocate memory for Data on the heap:
var d = cast[ptr Data](alloc0(sizeof(Data)))

# create a new string on the garbage collected heap:
d.s = "abc"

# tell the GC that the string is not needed anymore:
GCunref(d.s)

# free the memory:
dealloc(d)

帶 pointer 參數的 proc

proc peepEvents*(
    events: ptr Event; numevents: cint; action: EventAction;
    minKind: EventKind; maxKind: EventKind): cint {.
      cdecl, importc: "SDL_PeepEvents", dynlib: SDL2_LIB.}

取得變數的 address

var x: int
var px = addr x

或是

var x: int
var px = unsafeAddr x