$darkmode
Elektra 0.11.0
Language Bindings

Introduction

In this section, we will explain the how to write a language binding for Elektra.

High-level API

Writing bindings for the high-level API is described in a different document, since it is a bit less straightforward and needs additional considerations.

TODO

  1. which parts of Elektra bindings make sense (application, plugin, tools, ...)
  2. how to integrate bindings into CMake (if possible and useful)
  3. which parts of the bindings can and should differ for every language. This includes:
    1. iterators
    2. conversion to native types (strings, int, ...)
    3. operator overloading (if available)
    4. other programming language integrations (streams, hash-codes, identity, ...)
    5. returned errors from kdb functions (what this issue here is about)

CMake Integration

Building

To add the subdirectory containing our binding to the build, we have to modify src/bindings/CMakeLists.txt.

check_binding_included ("our_binding" IS_INCLUDED)
if (IS_INCLUDED)
add_subdirectory (our_binding_directory)
endif ()

At first we want to make sure that the build tools and compilers we need for the binding are installed. We can use find_program (BUILD_TOOL_EXECUTABLE build_tool) to find our build_tool program. The result of the search will be stored in BUILD_TOOL_EXECUTABLE, so now we can use an if block to include the bindings in the build, if the program exists or exclude it, if it doesn't. To do that, we use add_binding which adds ours to the list of bindings that will be built. For more provided functions, see here.

If, for example, our bindings only support linking against a dynamic library we can express that, by using the BUILD_* variables in if blocks or by passing ONLY_SHARED to add_binding. You can read more in the compile doc.

if (BUILD_TOOL_EXECUTABLE)
add_binding (our_binding ONLY_SHARED)
else ()
exclude_binding (our_binding, "build_tool not found")
return ()
endif ()

Elektra uses out-of-source builds, so we have to copy all the needed files over to the build directory. The ${CMAKE_CURRENT_SOURCE_DIR} variable refers to the source directory, while ${CMAKE_CURRENT_BINARY_DIR} refers to the build directory. The copy is as simple as

# Inside the previous if block
file (COPY "${CMAKE_CURRENT_SOURCE_DIR}/" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")

However for some files we may want to use some CMakes variables. Say we're writing a build script for our project and want to include the version number and the directory that libelektra.so resides in, so our build tool can find and link against it. We create our script named build.script.in. It looks like this

version = "@KDB_VERSION@"
link-search-path = "@CMAKE_BINARY_DIR@/lib"

Back in our CMake script, we tell CMake to replace the variables with their associated values.

configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/build.script.in" "${CMAKE_CURRENT_BINARY_DIR}/build.script" @ONLY)

Note how we leave off the .in ending on the target file.

Since we're building a foreign language project, it will most likely have its own build tool or compiler. So we have to tell CMake how to invoke it, in order to build the project. First we specify what file we expect to be generated by that command. In this example it's a .lib file that is generated in some target directory. We then call our build tool using the variable BUILD_TOOL_EXECUTABLE we created earlier with the build subcommand and the --release option. We can also specify one or multiple files that this command depends on, such that CMake can make sure they are built or generated before. Finally, we add a custom target that depends on the .lib file. To built this target, CMake will invoke our custom command and build the specified file.

add_custom_command (OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/target/release/libelektra.lib"
COMMAND ${BUILD_TOOL_EXECUTABLE} build --release
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/build.script" "${CMAKE_CURRENT_BINARY_DIR}/other-dependency.file")
add_custom_target (our_binding ALL DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/target/release/libelektra.lib")

We can then explicitly include the bindings using cmake -DBINDINGS="our_binding" .. in the build directory and follow the further steps for compilation.

Testing

To invoke our tests through CMake, we have to follow similar steps as in the build. We add a test by specifying a command that runs our tests. In our case, we're calling the same program for testing as in the building step.

add_test (NAME test_our_binding COMMAND ${BUILD_TOOL_EXECUTABLE} test WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")

We may have to specify an additional environment variable to tell the test command, where libelektra.so resides, so that the dynamic linker can find it.

set_property (TEST test_our_binding PROPERTY ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/lib")

Now our bindings can be tested through ctest alongside all other tests.

See the Java binding for examples.

Error Handling

Since v0.9.0, Elektra has a new error code system. You might want to take a look in the design decision first to understand the concept of the error codes. These codes are hierarchically structured and are therefore perfectly suitable for inheritance if the language supports it.

Some error codes like the Permanent Errors are generalizations and used for developers who want to catch all specific types of errors (e.g., it does not matter if it is a Resource or Installation Error but the developer wants to check for both). Such errors should not be able to "instantiate" or emitted back to Elektra as we want to force developers to take a more specific category. In case of Java for example the Permanent Error is an abstract class. Which errors are instantiable or not can be seen in the error-categorization guideline in the respective title saying either abstract or concrete. Here is an example of how Java has implemented it:

public abstract class PermanentException extends Exception {...}
public class ResourceException extends PermanentException {...}
public class MemoryAllocationException extends ResourceException {...}
public class InstallationException extends PermanentException {...}
...

All error codes as well as the hierarchy itself is depicted in the design decision.

If you have a language which does not support inheritance this way like GoLang, you can still use the error code itself since the hierarchy is integrated in it. For example you can check if the code starts with C01... to catch all Permanent Errors.

Error Message

In Elektra every error has a predefined format. You can take a look at the related design decision to see how it looks like.

Every Exception/Error struct/etc. should have separate accessors to individual parts of the message. These include:

  1. Module (getModule())
  2. Error Code (getErrorCode())
  3. Reason (getReason())
  4. Configfile (getConfigFile())
  5. Mountpoint (getMountpoint())
  6. Debuginformation (text looks like "At: file:line") (getDebugInformation())

In case of an error at least the following part has to be returned:

Sorry, module getModule() issued error getErrorCode():
getReason()

Please also keep the wording identical for consistency. Take a look how the Java binding implemented it in the KDBException