/cinterop

A C/C++ interop library for the Nim programming language

Primary LanguageNim

cinterop

Testing

A C/C++ interop library for the Nim programming language.

This project was directly inspired by the nimline library.

Overview

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.

Recommended compiler switches

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.

Showcase

Please see tests for examples of most features. This section provides an incomplete summary of the core functionality.

C++ Class interop

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.

Void returns

For expressions that evaluate to void, one can use the cexpr^ invocation, which is shorthand for cexpr[void]^:

cexpr^instance1.method1(0)

Type inference

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

Binary operations

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

Free function interop

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.

Enums

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.

Gotchas

Unary operations

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]^.

Initialization

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.

Installing

Thanks to @mantielero for adding initial support for nimble! The package can be installed by following the nimble instructions here.

Usage

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.

Contributing

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! 🍺