👑

Nimで動的配列のポインタを扱う

2023/02/04に公開

要約

Nim で動的配列のポインタを扱いたいときは,要素の型が同じシーケンス値を作ることで便利に実現できます:

# 実行時に配列長が `n` とわかったとき
var x = newSeq[T](n)

動的配列のポインタは,次のように取得できます:

addr x[0]

こうすることで,配列の値をポインタ経由で受け渡しできるようになる上に,std/sequtils などの seq[T] の資源も活用できます[1]

使いたい場面

純粋な Nim プログラムを書く場合は,ポインタを扱うべきではありません.
GC 管理される参照 ref T と比べて,ポインタ ptr T はメモリ管理を自力で行わなければならず,コードの複雑化を招いたりバグの原因になったりするためです.Nim でポインタが必要になる主な場面は,FFI を使いたいときでしょう.

例えば,次のような Vulkan API を叩く場合は有効です:

import nimgl/vulkan

vkLoad1_0()

var layerCount = 0'u32
discard vkEnumerateInstanceLayerProperties(addr layerCount, nil)

# ここで初めて,`availableLayers` の要素数が判明する.
var availableLayers = newSeq[VkLayerProperties](layerCount)
discard vkEnumerateInstanceLayerProperties(addr layerCount, addr availableLayers[0])

# API から受け取る `VkLayerProperties` 型の値は,`availableLayers` の各要素として
# 取り出すことができる.

なぜこれで実現できるか

シーケンスの先頭要素へのポインタを動的配列のポインタとして用いてよい,と述べましたが,本当にこれで問題ないのでしょうか.この疑問に答えるためには,seq[T] がどのように実装されているかを調べる必要があります.seq[T] の実装は選択しているメモリ管理手法によって異なるのですが,refc などの旧来の GC と ARC/ORC とで 2 つに大別できます[2].それぞれで見ていきましょう.

ARC/ORC の場合

ARC/ORC でメモリを管理する場合,Nim のシーケンス型 seq[T] の実体[3]UncheckedArray[T] にシーケンスの長さ情報をくっつけたものとなっています.

https://github.com/nim-lang/Nim/blob/f1519259f85cbdf2d5ff617c6a5534fcd2ff6942/lib/system/seqs_v2.nim#L16-L28

名前からもわかるように,シーケンスの要素は data に配列の要素として格納されます.つまり,シーケンス値の先頭要素へのポインタは,配列の(先頭要素への)ポインタと実質同じなのです!

ARC/ORC 以外の場合

それ以外のメモリ管理手法を選択した場合でも,seq[T] は配列長の情報がヘッダに付け加えられた配列として実装されています.ARC/ORC の場合と比べるとわかりにくいですが,実装は次のようになっています.

まず,シーケンス用のヘッダは TGenericSeq として定義されています.ARC/ORC のときと同じく,ヘッダにはシーケンスの長さが格納されます:

https://github.com/nim-lang/Nim/blob/f1519259f85cbdf2d5ff617c6a5534fcd2ff6942/lib/system.nim#L549-L556

シーケンスを作成するとき,メモリ領域は(ヘッダ分 + 要素分 len * typ.base.size)の大きさで確保されます:

https://github.com/nim-lang/Nim/blob/f1519259f85cbdf2d5ff617c6a5534fcd2ff6942/lib/system/gc.nim#L491-L497

作成後は,TGenericSeq 型のヘッダ Sup と配列 data[] との構造体としてシーケンス型の値にアクセスできます:

https://github.com/nim-lang/Nim/blob/13251c2ac9c7e76fb506eeb686a5e5d19d67d0de/compiler/ccgtypes.nim#L782-L812

シーケンスの要素は,同様に data の要素として格納されます.この実装でもやはり,シーケンス値の先頭要素へのポインタが配列のポインタとして機能することがわかります.

まとめ

Nim で動的配列のポインタを扱う方法として,シーケンス x の先頭要素へのポインタ addr x[0] を取得することをご紹介しました.なぜこの方法でよいのかについても,シーケンスの実装から明らかにしました.

脚注
  1. 他にも,malloccreate を使う方法等と異なりメモリ領域の解放を自力で行う必要がないという利点があります. ↩︎

  2. 後者は,コード内では seqV2 などと呼ばれているようです. ↩︎

  3. NimSeqV2[T] のことを指しています.要素は,p の参照先にある NimSeqPayload[T] 値の data に格納されますが,このメモリ領域は newSeqsetLen で渡された配列長の情報に基づいて決定されます: ↩︎

Discussion