C bindings
We distribute C and C++ bindings as a ZIP file for each supported platform with the following directory layout:
- Single C header file in
/include/dnp3.h
- Single C++ header file in
/include/dnp3.hpp
- CMake find package script in
/cmake/
- Platform-specific shared libraries in
/lib/<platform>
- License information in
/LICENSE.txt
A companion dependencies.txt containing 3rd party license information is also available on our download page. See the Dependency Licenses for more information.
The pre-built packages are available here:
- Windows x64 (Windows 7+)
- Windows x86 (Windows 7+)
- Linux x64 (kernel 2.6.32+, glibc 2.11+)
- Linux AArch64 (kernel 4.2, glibc 2.17+)
- ARMv7 Linux (kernel 3.2, glibc 2.17)
- ARMv6 Linux, hardfloat (kernel 3.2, glibc 2.17)
- ARMv6 Linux (kernel 3.2, glibc 2.17)
- MacOS x64 (MacOS 10.7+, Lion+)
MacOS x64 libraries are included in the package but are not officially supported. They are useful for developers using MacOS, but are not supported in production.
CMake Usage
Make the find package script discoverable by adding it to the prefix path. Next, call find_package
:
set(CMAKE_PREFIX_PATH ${DISTRIBUTION_PATH}/cmake)
find_package(dnp3 REQUIRED)
This call will expose three targets:
dnp3
which is a shared library. When using this target, you must make the DLL or .so file available to your executable. You can automatically copy it to the executable target directory using the following CMake statement:
add_custom_command(TARGET my_awesome_project POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:dnp3> $<TARGET_FILE_DIR:my_awesome_project>
)
dnp3_static
which is a static library. The library target includes information to link all the required system dependencies.
To use the static library on Windows, you must link with the static release C runtime using the /MT
flag.
Finally, declare a dependency on the library for your executable:
add_executable(my_awesome_project main.c)
target_link_libraries(my_awesome_project PRIVATE dnp3)
Mapping
Many of the concepts built into higher-level languages are just design patterns or idioms in C. When you see these higher levels patterns discussed in this guide, you can use the idioms to understand how they map to C.
Classes
C doesn't have classes with restricted member visibility. Instead, you can use opaque types to hide implementation details:
typedef struct opaque_type_t;
You can then define constructors
and destructors
as functions:
// constructor
opaque_type_t* create_opaque_type();
//destructor
void destroy_opaque_type(opaque_type_t* instance);
Class "member functions" are simply functions that take a pointer to the opaque type as the first argument:
int opaque_type_get_special_value(opaque_type_t* instance)
Interfaces
C accomplishes function polymorphism using function pointers. Interfaces are simply collections of function pointers along with some optional context. For example, let's look at the following logging interface in the library:
typedef struct dnp3_logger_t
{
void (*on_message)(dnp3_log_level_t level, const char* message, void* ctx);
void (*on_destroy)(void* ctx);
void* ctx;
} dnp3_logger_t;
ctx
is an opaque pointer to some state information required by the interface. It is passed into every method as the final argument.on_destroy
is the destructor that cleans up thectx
.on_message
is a function pointer used to dispatch a log message.
Keep in mind that this example only contains a single method. Other interfaces will contain several methods.
If your implementation of an interface is stateless, you can initialize ctx
and on_destroy
to NULL. C99 struct initialization syntax
will do this by default if you don't specify a value for these fields.
Iterators
Collections in the C bindings are always implemented as an opaque iterator type. Think of them as a class with a single "next" method. For example, let's look at an iterator over bool values:
// opaque iterator
typedef struct bool_iterator_t;
// next function
bool* bool_iterator_next(bool_iterator_t* iter);
If you are given this iterator in a callback, you can process the values in a loop:
void my_callback(bool_iterator_t* iter, void* ctx)
{
bool* value = NULL;
while(value = bool_iterator_next(iter))
{
// process value
}
}
Never use an iterator outside the callback. Frequently, the iterator points to memory on the stack and will result in undefined behavior if it is used after the callback is complete.
Error Handling
C API error handling is performed using error codes. An error code is always an enum value where the first value is equal to zero, indicating success.
Consider an error enum and a function that parses a string as an int
that can fail:
typedef enum my_error_t
{
MY_ERROR_OK = 0,
MY_ERROR_BAD_INT_STRING = 1,
} my_error_t;
my_error_t parse_string(const char* string, int* out);
When a function that can fail needs to return a value, it always does so using an out parameter as the last argument. Your code should always check for returned error values:
int value = 0;
my_error_t err = parse_string("not a number", &value);
if(err) {
printf("unable to parse number: %s\n", my_error_to_string(err));
// handle error
}
Every enum generated by the C code generator also has a generated *_to_string
helper method for debugging.