Binding Nim to C++ std::list
— Kaushal ModiHow to use the Nim importcpp pragma to bind to C++ Standard Template
Libraries like std::list.
I like ❤️ using Nim as my go-to programming language – be it for scripting, some CLI tool or any other application. The nice thing when using Nim is that you don’t have to throw away any of the past work done in C or C++, because it is pretty easy to bind those C/C++ libraries and extend their functionality in Nim.
To demonstrate that extensibility, in this post, I will add some new
API in Nim that builds on top of the std::list STL
The C++ STL is a set of template classes to provide common programming
data structures and functions such as lists, stacks, arrays, etc.
library. I won’t be re-writing any of the std::list methods in Nim
— No need to re-invent the std::list wheel in Nim! — I will
directly use the C++ std::list methods to build new API functions in
Nim. 🤯
Goal: Be able to easily access C++ std::list elements #
From the std::list reference, we can see that we cannot access a
list element by index using syntax like list_obj[index]. At the
end of this post, you will be able to convert C++ list objects to
Nim sequences and make the list data super-accessible — You can
print them using echo, use built-in libraries like sequtils, and
more.
importcpp pragma #
The first thing we’ll need is to be able to call the C++ list
methods directly in Nim, and we will need the importcpp pragma
This post will highlight some key features of this pragma. For full
details, check out this section in the Nim Manual linked in
References.
for that. This pragma is used to import a C++ method or type into
the Nim space.
This pragma is typically used like this:
{.link: "/path/to/libCppCode.so".}
type
MyNimType {.importcpp: "MyCppType", header: "libCppCode.hpp".} = object
proc myNimProc(): MyNimType {.importcpp: "myCppMethod", header: "libCppCode.hpp".}
var
foo = MyNimProc()
importcpp pragmaMyCppTypetype from the C++ library is mapped toMyNimTypeYou would typically name the type and method identifiers same same when binding external libraries to Nim, just to prevent any confusion. You can name them differently if you want to though. in the Nim code.myCppMethodfrom C++ is mapped tomyNimProc.- Then the code can call that
myNimProcin the Nim code along with other Nim stuff. - The
linkpragma is needed if we need to tell the Nim compiler where to find the implementation For the case of linking to the C++ STL<list>header, we do not need to provide thestd::listimplementation because the GNU GCC compiler provides that. of linked methods.
Bindings to the C++ <list> header #
Below code snippet shows us all the <list> header bindings we will
need to meet the goal of this post.
| |
std::list object types and methods- Line 2 binds the C++
std::listtype with the NimListtype. So, aList[cint]type in Nim get mapped tostd::list<int>in C++. - Line 3 binds the C++
iteratortype fromstd::listwith the Nim typeListIter.- Note special pattern
<'0>See 📖importcppfor procs for details on such special patterns. in theimportcppargument for this binding. The apostrophe followed by an integer i is replaced by the i’th parameter in theListIter[T]type i.e. theTtype. So,ListIter[cint]in Nim gets mapped tostd::list<int>::iteratorin C++.
- Note special pattern
- Line 5 binds the list constructor
()with the Nim procinitList[T](). As the constructor returns a list type, the return type on the Nim side set to the equivalentList[T].- Note the slightly different pattern in the
importcppargument for this binding:<'*0>. If we had not used that asterisk there, that would have mapped to the proc’s return type,List[T]. But we want the C++ mapped constructor to look likestd::list<int>()i.e. we needTand notList[T]in the C++ template type. The*does the job of derivingTfrom its “container” typeList[T].
- Note the slightly different pattern in the
- In lines 6 and 7,
sizeandbeginon C++ get mapped to same named procs on the Nim side, with the correct return types.csize_tis a Nim type that maps to the Csize_ttype.- As
beginreturns an iterator handle, the return type ofbeginisListIter[T]. - Note that I did not use the
std::listprefix for these mappings, because these procs will always be called in context of their arguments. For example,listIterVar.size()in Nim will map tolistIterVar.size()in C++..
- Line 8 maps the C/C++ indirection operator
*with Nim dereferencing operator[]. The input argument type is an iterator. So ifiteris a variable of typeListIter[cint],iter[]will return the value of typecintpointed to by that iterator.- The
importcppargument for this mapping has a different pattern:*#. The#is replaced by the first argument of the proc:iter. Soiter[]in Nim codeiter[]is same as writing`[]`(iter), but one might agree that the former style is better and more readable. will translate to*iter1 on the C++ side.
- The
Test C++ code #
We have the <list> bindings ready, but now we need some test C++
code which will be our “library” for use in the Nim code.
So here it is:
void generateList(int num, std::list<int> *retList)
{
for (int i = 0; i < num; ++i) {
retList->push_back(i * 2);
}
}
The C++ “library” here has a generateList API with expects an
integer num as the first arg, and it save a num element
std::list<int> object to the pointer passed as its second arg.
Now, we can either go the conventional route of compiling this code to
a shared object (.so or .dll) and then linking it to the Nim code
using the link pragma.
But.. that is boring 💤
We will do something cooler.. we will inline that C++ code in Nim
using the emit pragma and bind to it using importcpp just as we
did for the <list> methods! 😎
{.emit: """
void generateList(int num, std::list<int> *retList)
{
for (int i = 0; i < num; ++i) {
retList->push_back(i * 2);
}
}
""".}
proc generateList(num: cint; lPtr: ptr List[cint]) {.importcpp: "generateList(@)".}
emit pragma and then binding to it using importcppHere, I also introduce you readers to a new importcpp pattern:
@. The @ expands to comma-separated arguments passed on the Nim
side.
Note the equivalence between the C++ function signature
void generateList(int num, std::list<int> *retList) and Nim proc signature proc generateList(num: cint; lPtr: ptr List[cint]).
Test Nim code #
Below Nim code will call the test C++ generateList method defined
above and then print the whole list object after converting it to a
Nim sequence.
var
l = initList[cint]()
generateList(10, addr l)
var
s = l.toSeq()
echo "sequence = ", s
generateList method implemented in C++- In above snippet,
initListbindings defined in Code Snippet 2 is used to initialize the list objectl. - Its address or pointer is passed to
generateListwhich will populate the Nim allocated memory with 10 elements. - Then we call a
toSeqproc (that we will define in the next section) to convert that list to a Nim sequence. - Now that the list object is converted to a Nim sequence, printing it out as easy as you see above 😄.
Extending the std::list API in Nim #
The end goal is to be able to write a toSeq proc that can convert a
std::list object to a Nim sequence. Here’s a psuedo-code I began
before it evolved into Code Snippet 7.
proc toSeq (l: list_obj): sequence
lBegin = handle to the iterator for the beginning of list_obj; use `begin`
lSize = number of elements in list_obj; use `size`
lIter = lBegin
for idx in 0 ..< lSize:
sequence[idx] = *lIter; use the `[]` dereferencing operator
lIter = pointer to the next element; use `next` from <iterator>
toSeqAs it turned out, the real Nim code wasn’t longer or more complicated than the pseudocode 😆.
- Line 1 - We needed a binding to the
std::nextmethod from iterator so that we can increment the iterator starting from thebegin()returned value. - Line 4 - We already know the size of the list
object, so we pre-allocated the Nim sequence to the same size by
using the
newSeqproc. - Then rest of the code follows the idea presented in the pseudocode above.
Final Code #
The final code to (i) wrap the <list> types and methods and (ii) one
method from <iterator>, and (iii) define a proc for converting
std::list objects to Nim sequences was only 22 lines! (including
comments and blank lines)Click here to see the full code + output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
when not defined(cpp): {.error: "This file needs to be compiled with cpp backend: Run 'nim -b:cpp r <file>'.".}
## <list> bindings -- https://en.cppreference.com/w/cpp/container/list
type
List*[T] {.importcpp: "std::list", header: "<list>".} = object
ListIter*[T] {.importcpp: "std::list<'0>::iterator", header: "<list>".} = object
proc initList*[T](): List[T] {.importcpp: "std::list<'*0>()", constructor, header: "<list>".}
proc size*(l: List): csize_t {.importcpp: "size", header: "<list>".}
proc begin*[T](l: List[T]): ListIter[T] {.importcpp: "begin", header: "<list>".}
proc `[]`*[T](it: ListIter[T]): T {.importcpp: "*#", header: "<list>".}
## std::list processing procs and iterators
proc next*[T](it: ListIter[T]; n = 1): ListIter[T] {.importcpp: "next(@)", header: "<iterator>".}
proc toSeq*[T](l: List[T]): seq[T] =
result = newSeq[T](l.size())
var
it = l.begin()
for i in 0 ..< l.size():
result[i] = it[]
it = it.next()
## Test
when isMainModule:
{.emit: """
void generateList(int num, std::list<int> *retList)
{
for (int i = 0; i < num; ++i) {
retList->push_back(i * 2);
}
}
""".}
proc generateList(num: cint; lPtr: ptr List[cint]) {.importcpp: "generateList(@)".}
var
l = initList[cint]()
generateList(10, addr l)
var
s = l.toSeq()
echo "sequence = ", s
echo "first elem = ", s[0]
echo "elem 5 = ", s[5]
echo "last elem = ", s[^1]
std::list to a Nim sequence, with test C++ codesequence = @[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
first elem = 0
elem 5 = 10
last elem = 18
You can try this code by saving to a list_to_seq.nim file and
running nim -b:cpp r list_to_seq.nim.
Alternatively, you can paste this code on https://play.nim-lang.org/, set the ‘Compilation target’ to C++ and hit the yellow ‘Run!’ button.
Summary #
It becomes fairly easy to map any C++ (or C) library to Nim. Once that it done, you can leverage the power of Nim to extend that mapped library or manipulate data generated by that library. And you do not need to be a C++ programmer to do so (I am not).
The folks on Nim Forum, Discord and elsewhere have been very generous with their help and support, and that’s one of the main reasons it’s a joy to use Nim. 🙏
References #
- 📖 Nim Manual –
importcpppragma - C++
std::listreference - Nim
cppstlThis package contains bindings tostd::string,std::vectorandstd::complexas of . package – can be installed usingnimble install cppstl
I have almost no experience with coding in C++; I got help with this
importcpp: *#binding from user sls1005 on this Nim Forums thread. Thank you! ↩︎