“Hello user” example

It is time to introduce the most basic usage of CppBind. Let’s assume you wrote a fantastic 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.

Okay, here is your 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 other languages, you need to create a binding from your C++ library to that language. CppBind is a tool that helps you achieve that by adding some extra marks to your C++ codes.

In the code above, you need to export the UserInfo structure with age and name properties and the Host class with its methods. To do that, you need to change your Doxygen comments to include CppBind instructions and parameters. .. _Basic example C++ with CppBind:

#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?");
    }
};

We have just added the __API__ tag to let CppBind know what entity needs to be processed and added the instruction action: gen_method, which tells that a method needs to be generated. For the complete list of available instructions, see Generation instructions.

You can notice the usage of the throws variable in API comments. CppBind has required variables on some entities. In this case, we have set a mandatory throws variable on all methods/constructors. This requirement is done to ensure that the user hasn’t forgotten to mention possible exceptions that the entity can throw. More details can be found here. That is it. You should be able to use it on your codes written in supported languages. Here are usage examples for Kotlin, Python, and Swift.

package hello_user_usage

import com.hello_user.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 hello_user.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 Wrapper

@main
class HelloUser {

    static func main() {

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

        let 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?")
    }
}

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.

CppBind setup

To run CppBind, you need to have a config file for your project. The config file should include rules and definitions of minimal required parameters. CppBind provides a command-line utility to generate a default config file for your project. Run cppbind init in the project root directory to create the default project config file.

The content of the default config file provided by CppBind

vars:
  out_prj_dir: "."
  src_glob:
    - ./**/*.h*
  src_exclude_glob: []
  extra_headers:
    - stdexcept
    - new
    - typeinfo
  package_prefix: ""
  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')}}"
  python.pybind_module: ""
  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

The above-provided default config file can be changed later to fit the requirements of your project. For example, the default value the src_glob variable is a list with the file glob pattern corresponding to all the header files in the project. This can be modified to control the list of the files processed by CppBind. Some other variables (cxx_out_dir, out_dir, etc.) need to be adjusted to control the place where the generated bindings will be placed. You can find the whole list of CppBind variables here.

Note

From the default config file, you can notice the usage of the get_android_ndk_sysroot helper function when defining the clang_args variable for Kotlin target language on the macOS platform. ANDROID_NDK environment variable must be correctly set since it is used by get_android_ndk_sysroot function.

The content of "Hello user" project config file

type_vars:
  !join
  - !include std_exc_api.yaml

dir_vars:
  - dir: "."
    vars:
      project_dir: '{{_current_working_dir}}'
      project_link: 'https://github.com/PicsArt/cppbind/tree/master/examples/tutorials/hello_user'

vars: |
  {%- set out_prj_dir = "." -%}
  out_prj_dir: {{out_prj_dir}}
  src_glob:
    - cxx/**/*.h*
  include_dirs:
    - .
  extra_headers:
    - stdexcept
    - new
    - typeinfo
  mac.clang_args:
    - --sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
  file_postfix: ""
  kotlin.cxx_out_dir: {{path.join(out_prj_dir, "kotlin/wrappers")}}
  kotlin.out_dir: {{path.join(out_prj_dir, "kotlin/src/main/java")}}
  kotlin.package_prefix: com.hello_user
  kotlin.include_dirs:
    -  ./
    - /usr/lib/jvm/java-11-openjdk-amd64/include
  python.cxx_out_dir: {{path.join(out_prj_dir, "python/wrappers")}}
  python.out_dir: {{path.join(out_prj_dir, "python/src")}}
  python.package_prefix: hello_user
  python.pybind_module: hello_user
  swift.package_prefix: hello_user
  swift.cxx_out_dir: {{path.join(out_prj_dir, "swift/wrappers")}}
  swift.out_dir: {{path.join(out_prj_dir, "swift/src")}}

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

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

Bazel setups and rules for the “Hello user” example can be found here.