參考資訊
- Nim Manual
- Nim in Action 第 8 章
如何呼叫 C Function
需配合 pragma 告訴 Nim compiler 特定的 proc 其實是從 C 來的, 例如
proc exit(status: cint) {.importc.}
exit(0)
這是最簡單的型式
importc
告知 compilerexit()
是個 C functionstatus: 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)
跟其他 Library link
上面的例孑是呼叫 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
的使用時機差異目前還末瞭解.
加不加 header
的差異
以上例來說, 產生的 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 也變得不需要.
另外, dynlib
跟 header
是互斥的, 不可同時使用. 我們來改寫上面 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