Packaging the TWS C++ API as a CMake static library

Scope: This is a how-to for wrapping the Interactive Brokers TWS C++ API in a modern CMake static library you can consume from your own projects. It does not redistribute IBKR's source code or binaries. You must download the TWS API from Interactive Brokers and comply with their license for any use or modification.

Part 1 of this series covered building Intel's decimal floating-point library — the prerequisite for Decimal.h support in the IBKR API. This post picks up from there.

Why bother packaging it#

Interactive Brokers ships the TWS C++ API as a source distribution. The download does include a CMakeLists.txt, but it builds a shared library, targets C++11, has no protobuf generation step, and no Intel DFP integration. The Windows download also includes Visual Studio .sln files that are often tied to specific MSVC versions. Neither path gives you a clean, cross-platform static library you can consume as a submodule.

The right approach is a thin dedicated CMake project that wraps the IBKR sources as a static library with modern CMake target semantics, then consume it as a git submodule.

Why static, not shared? IBKR's C++ sources depend on protobuf for serialization. When it's built as a shared library, it becomes ABI-locked to a specific version of Protobuf. A static library defers protobuf linking to the consumer: your build system resolves protobuf once, for everything, with one version. The library is consumed via add_subdirectory, so whatever protobuf the consumer's find_package finds is what gets used — for both the wrapper and the consuming code.

ConsumerWrapperDependenciesyour_targetib_client_core(static library)bid(Intel DFP)protobuf::libprotobufAbseilws2_32 / -lpthread(platform)PRIVATEPUBLICPUBLICPUBLICPUBLICOne target_link_libraries(PRIVATE) call;PUBLIC propagation handles alltransitive deps automatically.

What to take from the IBKR download#

After downloading and unpacking the TWS API (stable release, from interactivebrokers.github.io), the relevant tree for C++ is:

IBJts/source/cppclient/client/   <- C++ API headers and sources
IBJts/source/proto/              <- .proto schema files (required for Order, Contract, etc.)

The cppclient/client/ directory is self-contained C++11. The key surfaces:

  • EClient / EClientSocket — initiates connections, sends requests to IB Gateway
  • EWrapper — pure-virtual callback interface; you implement this in your application
  • EReader — pumps incoming messages off the socket on a background thread
  • EDecoder / EDecoderUtils — decodes the binary wire protocol
  • Contract, Order, Execution — data types for market data subscriptions and order management
  • Decimal.h / Decimal.cpp — wraps Intel's BID decimal math (the reason Part 1 exists)

Keep every copyright header intact when you copy these files. IBKR's copyright notice is in every source file and must stay there.

Laying out the wrapper repository#

Create a private repository. The layout that works well:

ibkr-client-core/
├── CMakeLists.txt
├── include/              <- copy all *.h from cppclient/client/
├── src/                  <- copy all *.cpp from cppclient/client/
├── proto/                <- IBKR .proto files (commit these)
│   └── generated/        <- build-time output (.gitignore this)
└── extern/
    └── intel-dfp-math/   <- git submodule from Part 1

Separating headers into include/ and sources into src/ makes the public interface explicit: target_include_directories(... PUBLIC include) exposes exactly what consumers need without leaking build internals.

Prerequisites#

macOS:

brew install protobuf abseil

Linux:

apt install libprotobuf-dev protobuf-compiler libabsl-dev

Windows:

vcpkg install protobuf:x64-windows

The CMakeLists.txt#

Walking through the decisions in order.

Toolchain and standard#

cmake_minimum_required(VERSION 3.25)
project(ibkr-client-core CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if(NOT CMAKE_OSX_ARCHITECTURES)
    set(CMAKE_OSX_ARCHITECTURES "arm64")
endif()

CMAKE_CXX_STANDARD defaults to 23, but C++26 is passed directly via target_compile_options later. Why? CMake's AppleClang C++26 detection is incomplete — setting CMAKE_CXX_STANDARD 26 may not emit the right flag. Passing -std=c++26 directly is unambiguous. The IBKR sources are C++11 and compile without modification under C++23/26.

The CMAKE_OSX_ARCHITECTURES guard is macOS-only — on Linux/Windows it's ignored.

Homebrew prefix (macOS only):

if(APPLE)
    execute_process(
        COMMAND brew --prefix
        OUTPUT_VARIABLE BREW_PREFIX
        OUTPUT_STRIP_TRAILING_WHITESPACE
        RESULT_VARIABLE BREW_RESULT
    )
    if(BREW_RESULT)
        message(FATAL_ERROR "Homebrew not found. Install Homebrew or set CMAKE_PREFIX_PATH manually.")
    endif()
    list(APPEND CMAKE_PREFIX_PATH ${BREW_PREFIX})
endif()

This points find_package at Homebrew-installed packages. On Linux/Windows this block is skipped. The RESULT_VARIABLE check ensures a clear error if Homebrew is missing, rather than a confusing downstream failure from find_package.

Intel DFP#

add_subdirectory(extern/intel-dfp-math)
# 'bid' target and its PUBLIC include dirs are now available

Protobuf#

The IBKR API uses protobuf internally. find_package locates the protobuf installation; the loop below generates .pb.cc/.pb.h files at build time:

find_package(Protobuf REQUIRED)

set(PROTO_DIR     ${CMAKE_CURRENT_SOURCE_DIR}/proto)
set(PROTO_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto/generated)

file(MAKE_DIRECTORY ${PROTO_GEN_DIR})
file(GLOB PROTO_FILES CONFIGURE_DEPENDS "${PROTO_DIR}/*.proto")

set(PROTO_GENERATED_SRCS)
foreach(proto ${PROTO_FILES})
    get_filename_component(stem ${proto} NAME_WE)
    set(pb_cc "${PROTO_GEN_DIR}/${stem}.pb.cc")
    set(pb_h  "${PROTO_GEN_DIR}/${stem}.pb.h")
    add_custom_command(
        OUTPUT  ${pb_cc} ${pb_h}
        COMMAND $<TARGET_FILE:protobuf::protoc>
                --cpp_out=${PROTO_GEN_DIR}
                --proto_path=${PROTO_DIR}
                ${proto}
        DEPENDS ${proto}
        COMMENT "protoc: ${stem}.proto"
        VERBATIM
    )
    list(APPEND PROTO_GENERATED_SRCS ${pb_cc})
endforeach()

Each .proto gets its own add_custom_command so CMake tracks per-file dependencies: protoc reruns only for changed .proto files, and the generated .pb.cc/.pb.h become inputs to the library target like any other source. PROTO_GENERATED_SRCS accumulates only the .pb.cc files — CMake infers the .pb.h dependency automatically.

Why not use protobuf_generate_cpp()? That built-in CMake function is simpler but doesn't give you control over output directories. The explicit loop ensures generated files land in proto/generated/ on all platforms, keeping your build tree clean.

Abseil#

Abseil is a transitive dependency of protobuf. On Linux/Windows, protobuf's CMake config pulls Abseil in automatically. On macOS, Homebrew's protobuf does not propagate Abseil as CMake targets, so it must be linked explicitly. BREW_PREFIX is evaluated at configure time on the consumer's machine, so the path is always correct:

if(APPLE)
    file(GLOB ABSL_DYLIBS "${BREW_PREFIX}/lib/libabsl_*.dylib")
else()
    set(ABSL_DYLIBS "")
endif()

The library target#

file(GLOB SRCS_CPP CONFIGURE_DEPENDS "src/*.cpp")

add_library(ib_client_core STATIC
    ${SRCS_CPP}
    ${PROTO_GENERATED_SRCS}
)

Compiler options:

if(MSVC)
    target_compile_options(ib_client_core PRIVATE
        /std:c++latest
        /W3
        /wd4267  # size_t → int (common in IBKR sources)
        /wd4244  # possible loss of data
        /wd4996  # deprecated declarations
        /wd4800  # bool conversion
    )
else()
    target_compile_options(ib_client_core PRIVATE
        -std=c++26
        -fPIC
        -Wall
        -Wno-switch
        -Wpedantic
        -Wno-deprecated-declarations
        -Wno-nullability-extension
        -Wno-gcc-compat
        -Wno-unused-result
    )
endif()
set_target_properties(ib_client_core PROPERTIES CXX_STANDARD_REQUIRED OFF)

MSVC flags:

  • /std:c++latest — C++26 equivalent (latest supported draft)
  • /W3 — warning level 3 (not maximum; IBKR code is noisy)
  • /wd4267, /wd4244, /wd4996, /wd4800 — specific suppressions for common IBKR C++11 issues

Non-MSVC flags:

  • -std=c++26 — direct C++26 flag; passed directly rather than via CMAKE_CXX_STANDARD because CMake's AppleClang detection is incomplete
  • -fPIC — position-independent code (safe on static libs, required if the archive is ever linked into a shared library)
  • -Wno-* — suppress specific warnings from IBKR's C++11 sources under pedantic C++26

CXX_STANDARD_REQUIRED OFF prevents CMake from injecting its own standard flag on top of the one already in compile_options.

Public include directories and linking#

target_include_directories(ib_client_core PUBLIC
    include
    ${PROTO_GEN_DIR}
)

target_link_libraries(ib_client_core PUBLIC
    bid
    protobuf::libprotobuf
    ${ABSL_DYLIBS}
    $<$<PLATFORM_ID:Windows>:ws2_32>
    $<$<NOT:$<PLATFORM_ID:Windows>>:-lpthread>
)

PUBLIC on both means consumers automatically get the right include paths and transitive link dependencies.

Output layout#

The output layout block only activates when building standalone (not via add_subdirectory). It uses a portable approach that works across all generators and platforms:

if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    if(APPLE)
        set(_arch ${CMAKE_OSX_ARCHITECTURES})
    else()
        set(_arch ${CMAKE_SYSTEM_PROCESSOR})
    endif()

    set(_out "${CMAKE_SOURCE_DIR}/bin/${_arch}/$<LOWER_CASE:$<CONFIG>>")

    set_target_properties(ib_client_core PROPERTIES
        ARCHIVE_OUTPUT_DIRECTORY "${_out}"
    )

    if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke_test.cpp")
        add_executable(smoke_test tests/smoke_test.cpp)
        target_link_libraries(smoke_test PRIVATE ib_client_core)
        if(MSVC)
            target_compile_options(smoke_test PRIVATE /std:c++latest)
        else()
            target_compile_options(smoke_test PRIVATE -std=c++26)
        endif()
        set_target_properties(smoke_test PROPERTIES
            CXX_STANDARD_REQUIRED OFF
            RUNTIME_OUTPUT_DIRECTORY "${_out}"
        )
    endif()
endif()

The $<LOWER_CASE:$<CONFIG>> generator expression produces debug or release at build time and works for both single-config and multi-config generators.

Example output paths:

  • macOS arm64 debug: bin/arm64/debug/libib_client_core.a
  • Linux x86_64 release: bin/x86_64/release/libib_client_core.a
  • Windows MSVC debug: bin/AMD64/debug/ib_client_core.lib

When included via add_subdirectory, this block is skipped — the consumer controls output layout.

Verifying the build#

A smoke test that requires no live IB Gateway connection. It exercises both integration points — decimal math and protobuf — without touching a socket:

// smoke_test.cpp
#include "Decimal.h"
#include "Order.pb.h"

#include <print>
#include <string>
#include <string_view>

int main() {
    std::println("--- IBKR Client Core: Smoke Test ---");

    auto Verify = [](std::string_view testname, bool condition) {
        if (condition)
        {
            std::println("[PASS] {}", testname);
        }
        else
        {
            std::println(stderr, "[FAIL] {}", testname);
            std::exit(1);
        }
    };

    // 1. DECIMAL PRECISION
    Decimal d1  = DecimalFunctions::stringToDecimal("0.1");
    Decimal d2  = DecimalFunctions::stringToDecimal("0.2");
    Decimal sum = DecimalFunctions::add(d1, d2);

    std::string sumStr = DecimalFunctions::decimalStringToDisplay(sum);
    std::println("Math Verification: 0.1 + 0.2 = {}", sumStr);

    Verify("Precision Math (0.1 + 0.2 == 0.3)", sumStr == "0.3");

    // 2. DOUBLE ROUND-TRIP
    double  originalVal   = 1250.75;
    Decimal dVal          = DecimalFunctions::doubleToDecimal(originalVal);
    double  convertedBack = DecimalFunctions::decimalToDouble(dVal);

    Verify("Double Round-Trip (1250.75)", originalVal == convertedBack);

    // 3. PROTOBUF INTEGRATION TEST
    protobuf::Order order;
    order.set_action("BUY");
    order.set_orderid(1001);

    Decimal     qty    = DecimalFunctions::stringToDecimal("100.5");
    std::string qtyStr = DecimalFunctions::decimalStringToDisplay(qty);
    order.set_totalquantity(qtyStr);

    Decimal lmtPrice = DecimalFunctions::stringToDecimal("450.25");
    order.set_lmtprice(DecimalFunctions::decimalToDouble(lmtPrice));

    std::println(
        "Order Proto: {} {} at {}", order.action(), order.totalquantity(), order.lmtprice());

    Verify("Protobuf String Storage (Canonical)", order.totalquantity() == "100.5");
    Verify("Protobuf Field Access (OrderId)", order.orderid() == 1001);

    std::println("--- All Systems Functional ---");

    return 0;
}

A few things worth noting here. decimalStringToDisplay is not cosmetic — without it, stringToDecimal("0.3") can come back in scientific notation (+3E-1), which fails a string equality check even though the value is correct. The 0.1 + 0.2 test is the whole point of Part 1: binary double gives 0.30000000000000004; decimal gives 0.3. The protobuf test exercises the generated Order message — if it compiles and links, the protobuf integration is working.

Invocations#

macOS:

cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./bin/arm64/debug/smoke_test

Linux:

cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14
cmake --build build
./bin/x86_64/debug/smoke_test

Windows:

cmake -B build ^
    -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake ^
    -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --config Debug
.\bin\AMD64\debug\smoke_test.exe

Platform-specific summary#

Concern macOS Linux Windows
Homebrew brew --prefix N/A N/A
Abseil glob dylibs from Homebrew transitive from protobuf transitive from protobuf
Socket library not needed not needed ws2_32
Threading -lpthread -lpthread implicit (Win32)
Compiler flags -std=c++26 -std=c++26 /std:c++latest
Architecture CMAKE_OSX_ARCHITECTURES CMAKE_SYSTEM_PROCESSOR CMAKE_SYSTEM_PROCESSOR
Warnings -Wno-* (clang) -Wno-* (gcc/clang) /wd* (MSVC)
Static output lib*.a lib*.a *.lib

Consuming it in a downstream project#

Add the wrapper as a git submodule in your application repo:

git submodule add <your-private-repo-url> extern/ibkr-client-core
git submodule update --init --recursive   # pulls intel-dfp-math transitively

In the parent CMakeLists.txt:

add_subdirectory(extern/ibkr-client-core)
target_link_libraries(your_target PRIVATE ib_client_core)

That is it. CMake's PUBLIC propagation takes care of include paths and transitive link dependencies — Intel DFP, protobuf, pthreads. Your consuming target has no direct knowledge of those dependencies. It links to a single coherent target.

Maintenance#

When IBKR ships a new API version, update the submodule, re-copy the new cppclient/client/ headers and sources and proto/ files into your wrapper (preserving copyright headers in every file), and re-run the build. The protobuf generation step picks up schema changes automatically — no CMake changes needed.

Summary#

This wrapper turns Interactive Brokers' raw C++11 source distribution into a modern, cross-platform static library that you can drop into any CMake project as a git submodule.

  • Protobuf links once, at consumer build time — no ABI lock-in, no duplicate symbols, regardless of what else in your project uses protobuf.
  • Full out-of-the-box support for Intel DFP decimal math, protobuf code generation, and all three major platforms (macOS Apple Silicon, Linux, Windows).
  • One clean target_link_libraries call is all your application needs. CMake's PUBLIC propagation handles every transitive dependency automatically.

You get a professional-grade IBKR client core that feels like it was written for modern C++ toolchains — without touching IBKR's original code or license.


Disclaimer: This is not legal advice. Verify IBKR's current Non-Commercial and Commercial API license terms for your specific use case before deploying.

Share ->