C bindings
The C bindings consist of a single header and a shared library which makes application integration straightforward:
The distribution contains:
include/rodbus.h: the C API, in a single headerinclude/rodbus.hppandsrc/rodbus.cpp: the C++ wrapperlib/<target>/: the shared library for each platform, plus its import library on Windowscmake/: a CMake find package scriptLICENSE.txt,third-party-licenses.txtand the software bill of materialsrodbus-ffi.cdx.json
Each platform's folder in lib is named after its Rust target, which is also the value the CMake script expects:
| Platform | Target |
|---|---|
| Windows x64 (Windows 10+) | x86_64-pc-windows-msvc |
| Windows x86 (Windows 10+) | i686-pc-windows-msvc |
| Linux x64 (kernel 3.2+, glibc 2.17+) | x86_64-unknown-linux-gnu |
| Linux x64 (musl) | x86_64-unknown-linux-musl |
| Linux x86 (kernel 3.2+, glibc 2.17+) | i686-unknown-linux-gnu |
| Linux AArch64 (kernel 4.1+, glibc 2.17+) | aarch64-unknown-linux-gnu |
| Linux AArch64 (musl) | aarch64-unknown-linux-musl |
| Linux ARMv7hf (kernel 3.2+, glibc 2.17+) | armv7-unknown-linux-gnueabihf |
| Linux ARMv6hf (kernel 3.2+, glibc 2.17+) | arm-unknown-linux-gnueabihf |
| Linux ARMv6hf (musl) | arm-unknown-linux-musleabihf |
| Linux ARMv6 soft float (kernel 3.2+, glibc 2.17+) | arm-unknown-linux-gnueabi |
The CMake script is just a recipe for how you might integrate the library. You don't have to use it. Assuming that you've installed the shared library where the linker can find it, building a program can be as simple as:
> gcc main.c -I./c-bindings/include -lrodbus_ffi
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)
set(RODBUS_RUST_TARGET x86_64-unknown-linux-gnu)
find_package(rodbus REQUIRED)
The RODBUS_RUST_TARGET variable can also be set via the command line when configuring cmake. It just tells the script which library to link.
> mkdir build; cd build
> cmake .. -DRODBUS_RUST_TARGET=x86_64-unknown-linux-gnu
The find_package call exposes the rodbus library target. Declare a dependency on it for your executable:
add_executable(my_awesome_project main.c)
target_link_libraries(my_awesome_project PRIVATE rodbus)
Your executable needs the DLL or .so file at run time. You can copy it to the executable's directory after each build:
add_custom_command(TARGET my_awesome_project POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:rodbus> $<TARGET_FILE_DIR:my_awesome_project>
)
Mapping
Many concepts built into higher-level languages are just design patterns or idioms in C. Use the idioms below 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 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 rodbus_logger_t
{
void (*on_message)(rodbus_log_level_t level, const char* message, void* ctx);
void (*on_destroy)(void* ctx);
void* ctx;
} rodbus_logger_t;
ctxis an opaque pointer to some state information required by the interface. It is passed into every method as the final argument.on_destroyis the destructor that cleans up thectx.on_messageis a function pointer used to dispatch a log message.
This example contains only a single method; other interfaces contain several methods.
Always use C99 struct initialization when defining interfaces,
as this guarantees that fields you don't explicitly initialize are set to NULL. If your implementation is stateless, you can leave
ctx and on_destroy NULL.
Iterators
The C bindings implement collections as opaque iterator types. 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 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 its callback. Iterators typically reference stack memory, causing undefined behavior if used after callback completion.
Error handling
The C API uses error codes for error handling. Error codes are enum values where the first value equals 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.