C Bindings

The C bindings are distributed as a zip file with the following directory layout:

  • single header file in /include/dnp3.h
  • cmake find package script in /cmake/
  • platform specific shared libraries in /lib/<platform>
  • 3rd party license information in /lib/licenses.txt

CMake Usage#

Make the find package script discoverable by adding it to the prefix path. Then call find_package:

set(CMAKE_PREFIX_PATH ${DISTRIBUTION_PATH}/cmake)
find_package(dnp3 REQUIRED)

Then declare a dependency on the package:

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 simply design patterns or idioms in C. When you see these higher levels patterns discussed in this guide, you can use the idioms here 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#

Function polymorphism in C is accomplished using function pointers. Interfaces are simply collections of functions pointers along with some optional context. For example, consider 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 the ctx
  • on_message is a function pointer used to dispatch a log message.

This interface only contains a single method, but other interfaces contain a number of methods.

tip

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. You can think of them as a class with a single "next" method. Consider 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
}
}
warning

You should never use an iterator outside of the callback. Frequently, the iterator points to memory on the call stack and will result in undefined behavior if used after the callback has completed.

Error Handling#

Error handling in the C API is performed using error codes. An error code is always an enum value with the first value equal to zero and 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. User code should always check 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
}
note

Every enum generated by the C code generator also has a generated *_to_string helper method to aid in debugging.