• Basic Types
  • Pointer Types
  • Variable Types
  • Enum, Flags
  • Structure
  • Union
  • Pointer
  • Malloc & Free
  • Constructor & Destructor
  • Function Pointer
  • Array
  • String Array
  • C Macro
  • Error Handling
  • Experiment

Basic Types

以下這些定羲都在 system package 中

Nim <=> C C
culonglong (uint64) unsigned long long int
unsigned long ong
clonglong (int64) singed long long int
signed long long
long long int
long long
culong(uint32) unsigned long
clong(int32) signed long int
signed long
long
cuint(uint32) unsigned int
unsigned
cint (int32) signed int
signed
int
cushort(uint16) unsigned short int
unsigned short
cshort(int16) signed short int
signed short
short int
short
cschar(int8) signed char
cuchar(uint8) unsigned char
cchar(char, unsigned) char
clongdouble(BiggestFloat) long double
cdouble(float64) double
cfloat(float32) float
csize_t(uint) size_t
X (nim 裡面沒有 pointer 運算) ptrdiff_t
X intptr

其他帶長度的 type 例如 int8, float32 在現代的 C 中, stdint.h 裡都能找到對應的宣告, 且因為長度, 符號都固定, 所以都能簡單的對應到 nim 的 type 上.

其他參考資料

Pointer Types

Nim <=> C C
cstring char *
ptr cstring char ** (out parameter)
cstringArray(ptr UncheckedArray[cstring]) char ** (null terminated string array)
X const char *
X volatile
pointer void *

Variable Types

var

let

const

Enum, Flags

Structure

Union

Pointer

Malloc & Free

nim C
template alloc malloc()

Experiment

啟用新的 destructor 特性, 使用 malloc() 配置記憶體空間

nim c --newruntime -d:useMalloc --gc:arc --seqsv2:on test.nim

呼叫沒參數, 沒傳回值的 C Function

// test.c
#include <stdio.h>

void hello()
{
    printf("Hello\n");
}

配合 gorge()test.c 嵌在 .nim 裡, 簡化 bild 過程

# test.nim
{.emit: gorge("cat test.c").}

proc hello() {.importc.}

hello()

呼叫帶 string 參數的 C Function

// test.c
#include <stdio.h>

void hello(const char *name)
{
    printf("Hello %s\n", name);
}
# test.nim
{.emit: gorge("cat test.c").}

proc hello(name: cstring) {.importc.}

hello("Derek")

Build 過程 C compile 階段會報錯, 因為 nim 中 binding 的宣告與 .c 裡的沖突了, 可這麼解決

{.emit: gorge("cat test.c").}

type
    constcstring {.importc: "const char *".} = cstring

proc hello(name: constcstring) {.importc.}

hello("Derek")

呼叫傳回 string 的 C Function

// test.c
#include <stdio.h>

const char *hello()
{
    return "Hello";
}
# test.nim
{.emit: gorge("cat test.c").}

proc hello(): cstring {.importc, nodecl.}

echo hello()

這裡我們用了 nodecl pragma, 讓 nim 不要從 binding 宣告產生出 C code 從而避免跟 C 的宣告衝突. 不用, 因為我們在 nim code 中宣告傳回類型是 cstring, 因此會視回傳的 address 為可寫, 造成 runtime error (需要 C compiler + OS 的支援)

var s = hello()
s[0] = 'a'

Runtime 時

Traceback (most recent call last)
/home/derekdai/Projects/test-nim/src/test.nim(9) test
SIGSEGV: Illegal storage access. (Attempt to read from nil?)

修正 const char * hello() 的 Binding

// test.c
#include <stdio.h>

const char *hello()
{
    return "Hello";
}
# test.nim
{.emit: gorge("cat test.c").}

type
    constcstring {.importc: "const char *".} = cstring

proc hello(): constcstring {.importc, nodecl.}

var s = hello()
s[0] = 'a'

這樣, 在 C compile 階段, s[0] = 'a' 就會被補捉到對 readonly location 的寫入問題

.cache/nim/test_d/@mtest.nim.c:130:38: error: assignment of read-only location ‘*s__K4GHJYDHeml3uYswmICgbw’
  130 |  s__K4GHJYDHeml3uYswmICgbw[((NI) 0)] = 97;
      |                                      ^

但更好的, 應該是在 nim compiler time 就被偵測到問題.

在 nim 裡, 一個 type 是否能被修改, 除了 let, var, const 的 "變數" 層級宣告外, 更重要的是 operator 的 overriding.

上面我們宣告了 constcstring 只是個 cstring 的別名 (會有 cstring 的所有特徵, 例如各種的 operators), 這樣的話, constcstringcstring 繼承的 []= 會讓 s[0] = 'a' 通過 nim compile. 解決方式為宣告成 distinct type

type
    constcstring {.importc: "const char *".} = distinct cstring

Compiler 時

test.nim(9, 2) Error: type mismatch: got <constcstring, int literal(0), char>
but expected one of: 
proc `[]=`(s: var string; i: BackwardsIndex; x: char)
  first type mismatch at position: 0
proc `[]=`[I: Ordinal; T, S](a: T; i: I; x: sink S)
  first type mismatch at position: 0
proc `[]=`[Idx, T, U, V](a: var array[Idx, T]; x: HSlice[U, V]; b: openArray[T])
  first type mismatch at position: 0
proc `[]=`[Idx, T](a: var array[Idx, T]; i: BackwardsIndex; x: T)
  first type mismatch at position: 0
proc `[]=`[T, U, V](s: var seq[T]; x: HSlice[U, V]; b: openArray[T])
  first type mismatch at position: 0
proc `[]=`[T, U](s: var string; x: HSlice[T, U]; b: string)
  first type mismatch at position: 0
proc `[]=`[T](s: var openArray[T]; i: BackwardsIndex; x: T)
  first type mismatch at position: 0
template `[]=`(a: WideCStringObj; idx: int; val: Utf16Char)
  first type mismatch at position: 0
template `[]=`(s: string; i: int; val: char)
  first type mismatch at position: 0

expression: []=(s, 0, 'a')

constcstring 更通用

上例我們雖然正確的用 constcstring 對應了 C 的 const char *, 但同時也會讓以下功能不可用, 造成 constcstring 不是那麼通用

echo hello()[0]
echo hello()

例如第一行產生以下錯誤

test.nim(8, 13) Error: type mismatch: got <constcstring, int literal(0)>
but expected one of: 
proc `[]`(s: string; i: BackwardsIndex): char
  first type mismatch at position: 0
proc `[]`[I: Ordinal; T](a: T; i: I): T
  first type mismatch at position: 0
proc `[]`[Idx, T, U, V](a: array[Idx, T]; x: HSlice[U, V]): seq[T]
  first type mismatch at position: 0
proc `[]`[Idx, T](a: array[Idx, T]; i: BackwardsIndex): T
  first type mismatch at position: 0
proc `[]`[Idx, T](a: var array[Idx, T]; i: BackwardsIndex): var T
  first type mismatch at position: 0
proc `[]`[T, U, V](s: openArray[T]; x: HSlice[U, V]): seq[T]
  first type mismatch at position: 0
proc `[]`[T, U](s: string; x: HSlice[T, U]): string
  first type mismatch at position: 0
proc `[]`[T](s: openArray[T]; i: BackwardsIndex): T
  first type mismatch at position: 0
proc `[]`[T](s: var openArray[T]; i: BackwardsIndex): var T
  first type mismatch at position: 0
template `[]`(a: WideCStringObj; idx: int): Utf16Char
  first type mismatch at position: 0
template `[]`(s: string; i: int): char
  first type mismatch at position: 0

expression: [](hello(), 0)

第二行產生以下錯誤

test.nim(9, 6) Error: type mismatch: got <constcstring>
but expected one of: 
proc echo(x: varargs[typed, `$`])
  first type mismatch at position: 1
  required type for x: varargs[typed]
  but expression 'hello()' is of type: constcstring

expression: echo hello()

我們缺少了 [], 以及 constcstring 轉換成 string 的方法, 也就是 $. 配合 borrow pragma, 我們能從 cstring 那裡借一些 proc, 首先是 constcstring => string

proc `$`(s: constcstring): string {.borrow.}

這樣, echo 就能把 hello() 的回傳值印出來了.

接下來, []

proc `[]`(s: constcstring, i: int): char {.borrow.}

這次, 運氣不好, compiler 報錯

test.nim(6, 1) Error: no symbol to borrow from found

翻找了下 nim 的 source code, cstring 在 nim 裡面似乎並沒有太多可供操作的東西, 不外乎轉換成 string, [], []=, 且這些還是實驗得來的結果 (可能的 source code 位置: lib/system/strs_v2.nim). 不好, 好在, 這段我們能自行完成

{.emit: """
#ifndef _constcstring_char_at_
#define _constcstring_char_at_(s, i) ((s)[i])
#endif
"""}
proc `[]`(s: constcstring, i: Natural): char {.nodecl, importc: "_constcstring_char_at_".}
proc `$`(s: constcstring): string {.borrow.}

proc hello(): constcstring {.importc, nodecl.}

let x = hello()
echo x[1]

因為 nim 不少的 [] 實現, i 的 type 會宣告為 Natural, 所以不好對應成帶 type 的 function 參數 (其實在 [] 宣告時控制 i 的類型就能達成目的了), 所以這裡選擇了配合 macro 實現 [].

另外, echo 接受的參數為 varargs[string, `$`], 所以在傳入時自動的轉成了 string, 所以我們的 constcstring 加了 $ 後就能通過 compile.

呼叫傳回 char * 的 C Function

// test.c
#include <string.h>

char *hello()
{
    return strdup("Hello");
}
# test.nim
{.emit: gorge("cat test.c").}

proc hello(): cstring {.importc, nodecl.}
proc c_free(p: pointer) {.importc: "free", header: "<stdlib.h>".}

var s = hello()
echo s
c_free(s)

這裡 hello() 傳回的 char * 是用 strdup() 配置記憶體以後複雜過去的, 所以需要 c_free() 配合釋放

另外, C 的 string 常用是否為 const 作為 ownership 的區分, 例如 function 參數類型為 const char * 時, 代表 function 如果需要保留那個 string 時就必須 copy 一份.

呼叫傳回 Pointer of Incomplete Structure 的 C Function

fopen() 為例, 我們並不知道 FILE 內部的長像 (incomplete type), 這種情況通常只要簡單的宣告成 nim 的 pointer 即可

type
    FILE = pointer

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

var f = c_fopen("/etc/passwd", "r")
if f != nil:
    echo "closing file..."
    c_fclose(f)

呼叫由參數傳出 Pointer 的 C Function

有些 C function 的傳回值為 error code, 真正的結果由參數回傳, 這時候需要配合 addr 取得變數的 address 後傳入

// test.c
#include <string.h>
#include <errno.h>

int hello(char **v)
{
    *v = strdup("Hello");
    if(*v) {
        return 0;
    }

    return -ENOMEM;
}
{.emit: gorge("cat test.c").}

proc hello(v: ptr cstring): cint {.importc, nodecl.}
proc c_free(p: pointer) {.importc: "free", header: "<stdlib.h>".}

var s: cstring
if hello(addr s) == 0:
    echo s
    c_free(s)

呼叫傳回 Null Terminated String Array 的 C Function

C function 如果要傳回多個 string 傳不帶 length 參數時, 通常會配置一個 char * 的 array, 並將最後一個 element 設為 NULL 做為結束, 這種類型在 nim 裡可對應到 cstringArray 上.

另外, 很重要的一點, nim 跟你用的 library 可能 head 不是同一個, 所以, 雖說 nim 提供了 deallocCStringArray(), 但一定不要用它釋放 C function 回傳的 cstringArray, 或者應該這麼說, C 配置的東西就由 C 釋放, nim 配置的東西就由 nim 釋放, 混用一定是不安全的.

// test.c
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void hello(char ***ret)
{
    char **a = malloc(sizeof(char *) * 3);
    a[0] = strdup("Hello");
    a[1] = strdup("World");
    a[2] = NULL;

    *ret = a;
}

void free_strarray(char **a)
{
    char **p = a;
    while(*p) {
        free(*p);
        ++ p;
    }
    free(a);
}
{.emit: gorge("cat test.c").}

proc hello(a: ptr cstringArray) {.importc, nodecl.}
proc free_strarray(a: cstringArray) {.importc, nodecl.}

var a: cstringArray
hello(addr a)
echo $cstringArrayToSeq(a)
free_strarray(a)

在 nim 裡配置 C Struct 後傳給 C Function

要在 nim 中配置跟 C 一樣的 struct, 就得把完整的資訊告訴 nim compiler, 這樣, size 才會正確, 因此, C struct 的每個 field 都要正確無誤的對映到 nim 中的宣告

// test.c
#include <stdlib.h>
#include <string.h>
#include <errno.h>

typedef struct {
    char *name;
    int id;
} User;

void user_init(User *u, const char *name, int id)
{
    u->name = strdup(name);
    u->id = id;
}

void user_deinit(User *u)
{
    free(u->name);
    u->name = NULL;
}
{.emit: gorge("cat test.c").}

type
    User = object
        name: cstring
        id: cint

proc user_init(u: var User, name: cstring, id: cint) {.importc, nodecl.}
proc user_deinit(u: var User) {.importc, nodecl.}

var u: User
user_init(u, cstring("Derek"), cint(1))
user_deinit(u)

值得注意的是, 簡單的宣告為 object 類型即可. 並且, 在傳入 user_init() 時, C 需要的是 User *, 且因為會變動內容, 我們在第一個參數上宣告了 type 為 var User, 這樣 nim compiler 產生出來的 C code 才能通過 C compiler 的檢查.

如果, function 的參數類型沒有 var 時, nim 在傳遞 struct 給 C function 時就會以 by value 的方式, 這樣就會造成 C compile 的時候報錯. 要解決這個問題, 就要用上 byref pragma, 這樣 nim 在傳遞 User 時, 就算參數不是 var 也會以 pointer 傳入. 如下例的 user_id()

// test.c
#include <stdlib.h>
#include <string.h>
#include <errno.h>

typedef struct {
    char *name;
    int id;
} User;

void user_init(User *u, const char *name, int id)
{
    u->name = strdup(name);
    u->id = id;
}

int user_id(const User *u)
{
    return u->id;
}

void user_deinit(User *u)
{
    free(u->name);
}
{.emit: gorge("cat test.c").}

type
    User {.byref.} = object
        name: cstring
        id: cint

{.push, importc: "user_$1", nodecl.}
proc init(u: var User, name: cstring, id: cint)
proc deinit(u: var User)
proc id(u: User): cint
{.pop.}

var u: User
u.init(cstring("Derek"), cint(1))
echo u.id
u.deinit()

比較有意思的是, init(), id()deinit() 在 nim 中沒有加上 user_ 做為 prefix, 而是配合了 push pragma 把一些共通的 prama 保留到 pragma stack 中, 在接下來碰到 pop 前的宣告都會自動 apply 這些 pragma, 在手工 binding 時能省下大量的輸入量. 另外, 還能配合像 importc: "user_$1" 這樣去掉一些 import 進來 C function 的 prefix.

自動釋放 User.name

nim 在 1.2 版時加上了新的 runtime, 有了 destructor 及 move 語意能用來釋放及轉移資源, 以下是 destructor 的例子

{.emit: gorge("cat test.c").}

type
    User {.byref.} = object
        name: cstring
        id: cint

proc `=destroy`(u: var User) =
    echo "User destroyed"
    u.deinit()

{.push, importc: "user_$1", nodecl.}
proc init(u: var User, name: cstring, id: cint)
proc deinit(u: var User)
proc id(u: User): cint
{.pop.}

var u: User
u.init(cstring("Derek"), cint(1))
echo u.id
u.deinit()

在 Runtime 產生新的 User 並自動釋放

在 runtime 時 new 出來的 User 也是會被自動 destruct 的

discard new User

但是, 這麼寫 destructor 就不會被運行

var ur: ref User = new User

原因在於 ownership

var nu = new User
echo nu.type

會印出

owned ref User

修改一下變數的宣告, 確實 destructor 會被呼叫的

var nu: owned ref User = new User

所以, ref User 是 unowned reference, 因為沒有 ownership, 所以不使用資源時不應該被 destructor.

順帶一提, letvar 不影響 ownership, 所以前例中的 var 改成 let 並不影響結果

從 Function 中 init User 後回傳

看似簡單的操作, 其實有些學問. 在 function 中, 宣告了一個 User, 對它執行 init(), 所以 User.name 就指向了配置在 heap 中的 cstring, 然後 return 這個初始過的 User, 會發生什麼事?

proc newUser(name: string, id: int): User =
    var u: User

var u = newUser("Derek", 1234)

以上例而言, `=destroy` 會被執行兩次, 似乎正常, 但, 在 function 外的 u 能得到正確的, 初始後的值嗎? 我們修改一下 source code 確認下

// test.c
#include <stdlib.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

typedef struct {
    char *name;
    int id;
} User;

void user_init(User *u, const char *name, int id)
{
    u->name = strdup(name);
    u->id = id;
}

int user_id(const User *u)
{
    return u->id;
}

void user_deinit(User *u)
{
    printf("user_deinit: User(%p).name(%p) = %s, User.id=%d\n",
           u,
           u ? u->name : NULL,
           u ? u->name : NULL,
           u ? u->id : 0);
    free(u->name);
    u->name = NULL;
}
# test.nim
{.emit: gorge("cat test.c").}

type
    User {.byref.} = object
        name: cstring
        id: cint

{.push, importc: "user_$1", nodecl.}
proc init(u: var User, name: cstring, id: cint)
proc deinit(u: var User)
proc id(u: User): cint
{.pop.}
proc `$`(u: User): string =
    "User{name=" & $u.name & ",id=" & $u.id & "}\n"
proc `=destroy`(u: var User) =
    echo "User destroyed"
    u.deinit()

proc newUser(name: string, id: int): User =
    var u: User
    u.init(cstring(name), cint(id))

    u

var u = newUser("Derek", 1234)
echo u

輸出結果

1 User destroyed
2 user_deinit: User(0x7fff9ea04030).name((nil)) = (null), User.id=0
3 User{name=Derek,id=1234}
4 User destroyed
5 user_deinit: User(0x562d5975ec00).name(0x562d5a1302a0) = Derek, User.id=1234

上面輸出的 1-2 行, 其實是在對 newUser() 中的 result 進行清理動作, 然後再將 u 的內容 copy 給 result - 也就是說, nim 很聰明的沒有對 function 中的 u 再執行 destructor. 4-5 行則是 function 外的 u 被 destruct.

我們修改一下 newUser(), 直接對 result 執行 init()

proc newUser(name: string, id: int): User =
    result.init(cstring(name), cint(id))

從輸出來看

User{name=Derek,id=1234}
User destroyed
user_deinit: User(0x55ce39c3fc00).name(0x55ce3a52a2a0) = Derek, User.id=1234

減少了一次 destruct 以及 copy, 所以 result 的使用在 nim 裡是很重要的 (至少目前 nim 還沒優化到能認出這個多餘的操作)

轉移 Ownership 到 Proc 裡

配合 sink parameters, 可將 ownership 轉移到被執行的 proc 中

{.emit: gorge("cat test.c").}

type
    User {.byref.} = object
        name: cstring
        id: cint

{.push, importc: "user_$1", nodecl.}
proc init(u: var User, name: cstring, id: cint)
proc deinit(u: var User)
proc id(u: User): cint
{.pop.}

proc `$`(u: User): string =
    "User{name=" & $u.name & ",id=" & $u.id & "}"
proc `=destroy`(u: var User) =
    u.deinit()

proc consumeUser(u: sink User) =
    echo "in consumeUser"
    discard

var u: User
u.init("Derek", 1234)

consumeUser(u)
echo "back to main"

執行結果... 會 double free, 搞不定

in consumeUser
user_deinit: User(0x7ffd7fc84900).name(0x55aff63ca2a0) = Derek, User.id=1234
back to main
user_deinit: User(0x55aff48205e0).name(0x55aff63ca2a0) = , User.id=1234
free(): double free detected in tcache 2
Traceback (most recent call last)
/home/derekdai/Projects/test-nim/src/test.nim(16) test
/home/derekdai/Projects/test-nim/src/test.nim(17) =destroy
SIGABRT: Abnormal termination.

以下這段 `=sink` 也沒試出來...

nim compiler 在加了 move 語意後, 就能很好的處理這種情況, 因為 function 內的 User 必定會執行 destructor, 所以, 在此之前就會把值 copy 給 function 外的 User, 然後, function 內的 User 重新初始為 nim 裡相對應 type 的 default value (或是 golang 的術語, zero value), 接著在離開 function 前 destructor 被呼叫, 所以 User.name 安全的沒有被 free 掉. 在 function 外的 User 拿到了正確的值, 且最終 destruct 時也能正確的被釋放, 很是方便.

lent

lent type
之後補充

Ref Type 跟 Non-Ref Type 的差異

Object Construction 這節有提到:

For a ref object type system.new is invoked implicitly.

所以, 下面的 o1 會在 stack 上, o2 會在 heap 上

type
    MyObj = object
    MyObjRef = ref MyObj

var o1 = MyObj()
var o2 = MyObjRef()
也就是說, 像 `GObject` 這樣的類型, 基本上都是配置在 heap 上, 所以就會對應到 nim 的 `ref` type. 如果是

proc type & Closure

proc type 當成參數傳入其他的 proc

type
    Cb = proc(): int

proc addCb(v: int, cb: Cb): int =
    result = v + cb()

proc one(): int = 1

echo addCb(10, one)

也可以這麼寫

proc addCb(v: int, cb: proc(): int): int =
    result = v + cb()

proc one(): int = 1

echo addCb(10, one)

或是這麼寫

proc addCb(v: int, cb: proc(): int): int =
    result = v + cb()

echo addCb(10, proc(): int = 1)

還可以這麼玩

proc addCb(v: int, cb: proc(): int): int =
    result = v + cb()

proc closure(v: int): proc(): int =
    result = proc(): int = v

echo addCb(10, closure(10))

這裡有一個地方要注意, 不能這麼寫, nim compiler 會報錯

proc closure(v: int): proc(): int =
    proc(): int = v

nim 的 proc types 比較特別的地方是, calling convention 也是 signature 的一部份, 這樣就能在 compile 階段就抓出問題來.

以上例而言, 在沒有加任何 calling convention pragma 的情況下, 用的是會是 nimcall. nimcallproc type 與 closure 相容, 所以例子中, 我們能將 nim proc 當成參數傳入 (closure calling convention, 會帶上一個隱藏的參數, 保存著產生的當下的 context, 或是 environment).

Function Pointer

在 nim 裡可用 proc type 對應

Nim Proc As C Callback

Tuple 跟 Object 有什麼差異

Tuples and object types 這一節有提到幾點

  • 兩者都是用做不同類型數據的 container
  • tuple 無法被繼承
  • tuple 只要 field name, field type, filed order 皆相同的情況下就算是相同的 type
  • tuple 在 memory layout 上非常單純, 不會加上宣告的 field 外的東西
  • tuple 在 instantiate 時, 可以不用給 field name
    ... 未完