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.
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 GatewayEWrapper— pure-virtual callback interface; you implement this in your applicationEReader— pumps incoming messages off the socket on a background threadEDecoder/EDecoderUtils— decodes the binary wire protocolContract,Order,Execution— data types for market data subscriptions and order managementDecimal.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 viaCMAKE_CXX_STANDARDbecause 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_librariescall is all your application needs. CMake'sPUBLICpropagation 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.