A C/C++ interop library for the Nim programming language.
This project was directly inspired by the nimline library.
Similar to nimline, this library allows one to interop with C/C++ code without having to create wrappers. Unlike nimline, cinterop does not depend on Nim's experimental dotOperators feature and relies only on Nim's macro system to generate code.
Features include:
- No dependencies other than Nim's standard library.
- Convenience macros to declare C/C++ types and functions (decls.nim).
- Conversion of a subset of Nim to its syntactical equivalent in C/C++ without requiring forward declarations (exprs.nim).
This project is not a replacement for hand-written wrappers or wrapper
generators like c2nim. This library is
useful for quickly prototyping new code that depends on large C/C++
libraries, and is carefully designed so code can progressively be migrated to
use Nim's header
and importcpp
pragmas directly.
This project uses Nim's ARC and C++17. It is well tested with non-trivial code using the Visual Studio compiler on Windows. The recommended compiler switches are indicated at the top of the main test file.
Please see tests for examples of most features. This section provides an incomplete summary of the core functionality.
Say you have the following C++ class:
// simple.hpp
class CppClass1
{
public:
int field1 = 1;
int method1(int arg)
{
return 1 + arg;
}
};
You simply need to declare the C++ type and the source file it resides in:
# simple.nim
import cinterop/decls
csource "simple.hpp":
type CppClass1* = object of CClass
and then you can access the fields and methods of that type:
# main.nim
import cinterop/exprs
var instance1 = CppClass1.init()
echo cexpr[cint]^instance1.field1 # prints "1"
cexpr[cint]^instance1.field1 = 2
echo cexpr[cint]^instance1.method1(instance1.field1) # prints "3"
Notice that cexpr[T]^
indicates the return type T
of the whole expression,
and only needs to be used at the beginning. This means that types for members do
not need to be declared, as long as the type of the variable whose members are
accessed is known.
For expressions that evaluate to void
, one can use the cexpr^
invocation,
which is shorthand for cexpr[void]^
:
cexpr^instance1.method1(0)
If the type of a return value does not need to be known but is used in an
operation, one can use the cauto^
invocation like so:
cauto^instance1.field1 += 2
A cexpr[T]^
invocation can appear on either side of a binary operation.
cauto^
can only be used on the right-hand side unless the left-hand side is
also a cauto^
invocation. Examples:
cexpr[cint]^instance1.field1 += cexpr[cint]^instance1.field1
cauto^instance1.field1 += cexpr[cint]^instance1.field1 # same as above
cauto^instance1.field1 += cauto^instance1.field1 # same as above
The following technique can be used for libraries with lots of functions that don't hang off of classes:
# glfw3.nim
csource &"{GLFW}/glfw3.h": # header file
type cglfw* {.cgen:"(glfw$1(@))".} = object of CClass
# canvas.nim
...
cauto^cglfw.GetMouseButton(self.window, button) == 1
# generates something like `glfwGetMouseButton(self.window, button) == 1`
...
cglfw
here serves as a namespace that is not visible in C++. The cgen
pragma
tells the compiler how cglfw.GetMouseButton(self.window, button)
should be
generated and has the same semantics as Nim's importcpp
pragma.
For working with C/C++ enums, one can use the cenum
pragma like so:
type CPP_ENUM* {.cenum.} = object
Although enums can be emulated using CClass
and static methods, cenum
provides better checking of enum semantics and produces better C/C++ code for
enums. By default, enum field access results in an expansion similar to the
following:
let enumValue = cauto^CPP_ENUM.MEMBER_1
# generates `CPP_ENUM enumValue = CPP_ENUM_MEMBER_1`
Custom code generation for enums can be achieved using cgen
as well.
Unary operators cannot be used with cexpr[T]^
and cauto^
invocations
without using parentheses:
# echo -cauto^instance1.field1 # error
echo -(cauto^instance1.field1) # compiles
There is a proposal to avoid this
issue and enable a more natural implementation of cexpr[T]^
.
cauto^
can be used on the right-hand side of an initialization, but doing so
may cause backend compile errors, especially if done at the global scope:
let value = cauto^instance1.field1 # C++ backend may produce an error here
If this issue is encountered, the workaround is to explicitly specify the type:
let value = cexpr[cint]^instance1.field1
Other issues are documented in the tests.
Thanks to @mantielero for adding initial support for nimble! The package can be installed by following the nimble instructions here.
Typical usage is to import cinterop/decls
in modules that declare C/C++ types,
and to import those modules along with cinterop/exprs
to make use of them in
other modules.
This project is maintained during my free time, and serves as a tool for a game engine I am writing after work hours. Contributions are welcome, and I will merge them immediately if they serve to keep the project robust, simple, and maintainable.
Cheers and happy coding! 🍺