Libecasoundc: Pure C Wrapper for Ecasound C++

This article explains how libecasoundc functions as a bridge between pure C applications and Ecasound's object-oriented C++ engine. Ecasound is a powerful multitrack audio processing tool written natively in C++, making direct integration difficult for software written in C or languages relying on standard C Foreign Function Interfaces (FFI). Through the use of extern "C" linkage, opaque handles, type marshalling, and internal exception containment, libecasoundc abstracts the underlying C++ classes into a straightforward, procedural C API.

C Linkage and Name Demangling

The primary barrier between C and C++ is ABI compatibility, particularly C++ symbol name mangling and calling conventions. To solve this, libecasoundc defines its public headers within an extern "C" block.

When compiled, this directive instructs the C++ compiler to retain standard C symbol naming conventions and stack-frame calling conventions for the exposed functions. Consequently, standard C linkers and third-party language bindings can locate and invoke functions like eci_init(), eci_command(), and eci_cleanup() without requiring knowledge of C++ ABI internals.

The Ecasound Control Interface (ECA-CI) Abstraction

Ecasound’s core architecture is driven by the Ecasound Control Interface (ECA-CI). In the native C++ engine, ECA-CI is implemented as a set of classes (primarily ECA_CONTROL_INTERFACE) that accept text-based commands, process audio graphs, and return state information.

libecasoundc acts as a thin procedural wrapper around this control interface:

  1. Instance Management: The wrapper handles the instantiation and destruction of the underlying C++ ECA_CONTROL_INTERFACE objects. Depending on whether the standard or re-entrant version of the library is used, this instance is maintained either as an internal global pointer or passed through an opaque context handle.
  2. Procedural Mapping: Object-oriented methods are mapped to procedural functions. For example, a method call like eci->command("start") is wrapped inside the procedural function eci_command("start").

Data Marshalling and Type Conversion

C cannot natively interpret standard C++ Standard Template Library (STL) containers such as std::string, std::vector, or native stream buffers. The libecasoundc implementation file is compiled with a C++ compiler, allowing it to freely access both C and C++ types.

When a C caller sends data into libecasoundc, strings are passed as null-terminated const char* pointers. The wrapper internally converts these pointers into std::string objects before invoking the C++ core methods.

When returning data back to the C caller:

Exception Handling and Error Isolation

C lacks native support for C++ exception handling. If an unhandled C++ exception unwinds the stack across a standard C call boundary, it results in undefined behavior and typically causes the host process to terminate.

To prevent this, libecasoundc wraps internal calls to the C++ core within standard try...catch blocks. If the C++ engine throws an exception during command parsing or audio graph setup, the wrapper catches the error internally, sets an internal error flag, and translates the failure into a C-compatible return value or stores an error message that can be queried using procedural functions such as eci_last_error().