Skip to content

"Hello user" tutorial

Introducing the most basic usage of CppBind. Assuming you wrote a library that greets its user. And, of course, you don't want to restrict it only to C++ users. You want to make it available for other language programs as well.

Let's consider following C++ code:

#include <string>

/**
 * Structure to describe user.
 */
struct UserInfo {

    /**
     * Creates user
     */
    UserInfo(const std::string& user_name, unsigned int user_age) : age(user_age), name(user_name) {}
    /**
     * Age of user.
     */
    unsigned int age = 0;
    /**
     * Name of user.
     */
    std::string name;
    /**
     * Some wishes of the user.
     */
    bool want_a_drink = false;
};

/**
 * Host class.
 */
class Host {
public:
    /**
     * Creates host
     */
    Host() = default;
    /**
     * Greeting function.
     */
    std::string hello(const UserInfo& user) {
        return (user.age > 21 ? "Hello ": "Hi ") + user.name;
    }
    /**
     * Welcome function.
     */
    std::string welcome(const UserInfo& user) {
        if (!user.want_a_drink)
            return "Welcome " + user.name + "! Let me know if you want something.";
        return "Welcome " + user.name + "! Do you want cap of " + (user.age > 21 ? "beer?": "juice?");
    }
};

To use this from another language, you need to create a binding from your C++ library to the target language. We are going to generate the bindings with CppBind.

In the code above, you need to export the UserInfo structure with age and name properties and the Host class with its methods. In order to generate bindings, as a first step you need to configure your project. You can use the default configuration provided by CppBind with running cppbind init command in your project root directory. This will create a cppbind.yaml file containing the following configuration:

CppBind default configuration
vars:
  out_prj_dir: "."
  src_glob:
    - ./**/*.h*
  extra_headers:
    - stdexcept
    - new
    - typeinfo
  include_dirs:
    - .
  kotlin.clang_args:
    - -D__ANDROID__
  mac.kotlin.target_arch: x86_64
  mac.kotlin.clang_args:
    - -D__ANDROID__
    - --target={{target_arch}}-none-linux-android
    - --sysroot={{get_android_ndk_sysroot(getenv('ANDROID_NDK'))}}
  mac.python.clang_args:
    - --sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
  mac.swift.clang_args:
    - --sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
  kotlin.cxx_out_dir: "{{path.join(out_prj_dir, 'kotlin/wrappers')}}"
  kotlin.out_dir: "{{path.join(out_prj_dir, 'kotlin/src/main/java')}}"
  python.cxx_out_dir: "{{path.join(out_prj_dir, 'python/wrappers')}}"
  python.out_dir: "{{path.join(out_prj_dir, 'python/src')}}"
  swift.cxx_out_dir: "{{path.join(out_prj_dir, 'swift/wrappers')}}"
  swift.out_dir: "{{path.join(out_prj_dir, 'swift/src')}}"

type_vars:
  !join
  - !include std_exc_api.yaml

var_def:
  !join
  - !include variable_definitions.yaml

rules:
  kotlin.code_snippets:
    !join
    - !include kotlin/code_snippets.yaml
  python.code_snippets:
    !join
    - !include python/code_snippets.yaml
  swift.code_snippets:
    !join
    - !include swift/code_snippets.yaml

  kotlin.type_converters:
    !join
    - !include "kotlin/*_types.yaml"
  python.type_converters:
    !join
    - !include "python/*_types.yaml"
  swift.type_converters:
    !join
    - !include "swift/*_types.yaml"

  kotlin.actions:
    !join
    - !include kotlin/actions.yaml
  python.actions:
    !join
    - !include python/actions.yaml
  swift.actions:
    !join
    - !include swift/actions.yaml

Above provided default config file can be changed later to fit the requirements of your project. For example, we can consider the change of the src_glob variable. This variable defines the source files which are going to be processed by CppBind. The default value of this variable corresponds to all header files in the project. In our example we have changed the value of src_glob, since our source files are located under cxx directory. More information about CppBind variables can be found here.

Note

When generating bindings on the macOS platform for Kotlin, you need the ANDROID_NDK environment variable to be set. It should point to the directory where android NDK is installed. CppBind requires ANDROID_NDK environment variable to populate the correct clang arguments.

After project configuration is done, you need to add CppBind instructions and parameters in the doxygen style comments of your C++ code.

#include <string>

/**
 * Structure to describe user.
 * __API__
 * action: gen_class
 * package: hello
 */
struct UserInfo {

    /**
     * Creates user
     * __API__
     * action: gen_constructor
     * throws: no_throw
     */
    UserInfo(const std::string& user_name, unsigned int user_age) : age(user_age), name(user_name) {}
    /**
     * Age of user.
     * __API__
     * action: gen_property_getter
     */
    unsigned int age = 0;
    /**
     * Name of user.
     * __API__
     * action: gen_property_getter
     */
    std::string name;
    /**
     * Some wishes of the user.
     * __API__
     * action: gen_property_setter
     */
    bool want_a_drink = false;
};

/**
 * Host class.
 * __API__
 * action: gen_class
 * package: hello
 */
class Host {
public:
    /**
     * Creates host
     * __API__
     * action: gen_constructor
     * throws: no_throw
     */
    Host() = default;
    /**
     * Greeting function.
     * __API__
     * action: gen_method
     * throws: no_throw
     */
    std::string hello(const UserInfo& user) {
        return (user.age > 21 ? "Hello ": "Hi ") + user.name;
    }
    /**
     * Welcome function.
     * __API__
     * action: gen_method
     * throws: no_throw
     */
    std::string welcome(const UserInfo& user) {
        if (!user.want_a_drink)
            return "Welcome " + user.name + "! Let me know if you want something.";
        return "Welcome " +  user.name + "! Do you want cap of " + (user.age > 21 ? "beer?": "juice?");
    }
};

Here we have just added the __API__ tag to start CppBind annotation for this particular method and added the instruction action: gen_method, which tells CppBind that a binding needs to be generated for this method. For the complete list of available instructions, see Generation instructions.

You can notice the usage of the throws variable in API comments. There are variables which are required on some entities. In this case, the throws variable is required on all methods/constructors. This requirement is added to ensure that the user hasn't forgotten to mention possible exceptions that the method/constructor can throw. More details can be found here.

That is it. You should be able to use it from your codes written in target languages. Here are usage examples for Kotlin, Python, and Swift.

package hello_user_usage

import example_lib.hello.*


class HelloUserApp {

    companion object {

        init {
            System.loadLibrary("wrapper_jni")
        }

        @JvmStatic
        fun main(args: Array<String>) {

            val user = UserInfo("John", 22)
            val young_user = UserInfo("Kate", 18)

            val host = Host()

            assert(host.hello(user) == "Hello John")
            assert(host.hello(young_user) == "Hi Kate")

            assert(host.welcome(user) == "Welcome John! Let me know if you want something.")
            assert(host.welcome(young_user) == "Welcome Kate! Let me know if you want something.")

            user.want_a_drink = true
            young_user.want_a_drink = true

            assert(host.welcome(user) == "Welcome John! Do you want cap of beer?")
            assert(host.welcome(young_user) == "Welcome Kate! Do you want cap of juice?")

       }

    }
}
from example_lib.hello.hello_user import UserInfo, Host

user = UserInfo(user_name="John", user_age=22)
young_user = UserInfo(user_name="Kate", user_age=18)

host = Host()

assert host.hello(user=user) == "Hello John"
assert host.hello(user=young_user) == "Hi Kate"

assert host.welcome(user=user) == "Welcome John! Let me know if you want something."
assert host.welcome(user=young_user) == "Welcome Kate! Let me know if you want something."

user.want_a_drink = True
young_user.want_a_drink = True

assert host.welcome(user=user) == "Welcome John! Do you want cap of beer?"
assert host.welcome(user=young_user) == "Welcome Kate! Do you want cap of juice?"
import XCTest
import Wrapper

func runHelloUserTests() {

    let user = UserInfo(userName: "John", userAge: 22)
    let young_user = UserInfo(userName: "Kate", userAge: 18)

    let host = Wrapper.Host()

    assert(host.hello(user: user) == "Hello John")
    assert(host.hello(user: young_user) == "Hi Kate")

    assert(host.welcome(user: user) == "Welcome John! Let me know if you want something.")
    assert(host.welcome(user: young_user) == "Welcome Kate! Let me know if you want something.")

    user.want_a_drink = true
    young_user.want_a_drink = true

    assert(host.welcome(user: user) == "Welcome John! Do you want cap of beer?")
    assert(host.welcome(user: young_user) == "Welcome Kate! Do you want cap of juice?")

}

#if os(Linux)
runHelloUserTests()
#elseif os(OSX)
class HelloUserTests: XCTestCase {
    func testRun() throws {
        runHelloUserTests()
    }
}
#endif

The above-described steps are applicable for all the new classes and functions you want to expose to other languages. Besides that, you need to configure the CppBind project file once to use the tool. To know more about the CppBind configuration, continue reading.

After generating bindings, you should include them with C++ source codes in your project build. In our tutorial, we use Bazel to build the project.

Bazel setups and rules for the "Hello user" tutorial can be found here.


Last update: December 1, 2022