idax
The IDA SDK, redesigned for humans.
C++23 · Fully opaque · Zero SDK leakage · std::expected error model
idax is a comprehensive, opaque, domain-driven C++23 wrapper over the IDA Pro SDK. It replaces the SDK's raw C-heritage API surface with a consistent, self-documenting interface designed to be understood on first contact, without sacrificing any of the power that makes IDA the industry standard.
#include <ida/idax.hpp>
// Open a database, iterate functions, decode instructions, decompile.
// No flags. No sentinel values. No manual locking. No raw structs.
ida::database::init();
ida::database::open("firmware.i64", /*auto_analysis=*/true);
ida::analysis::wait();
for (auto fn : ida::function::all()) {
if (ida::instruction::is_call(fn.start()))
ida::comment::set(fn.start(), "entry is a call instruction");
}
auto main_address = ida::name::resolve("main");
if (main_address) {
auto df = ida::decompiler::decompile(*main_address);
if (df) {
auto lines = df->lines();
if (lines) {
for (const auto& line : *lines)
std::cout << line << "\n";
}
}
}
ida::database::save();
ida::database::close();The IDA SDK is one of the most powerful binary analysis interfaces ever built. It is also one of the most hostile to learn.
After years of writing plugins, loaders, and processor modules against the raw SDK, a pattern emerges: most of the difficulty isn't the concepts --- it's the API surface. Abbreviated names (segm, func, cmt), packed bitfields with magic constants, inconsistent error conventions, sentinel values masquerading as success, pointer invalidation traps, include-order dependencies, and six different ways to do the same thing with subtly different semantics.
idax was born from a simple observation: the IDA SDK's power is extraordinary, but its usability is not proportional to that power. The wrapper exists to fix that ratio.
-
Domain-driven, not header-driven. The SDK organizes by internal implementation files. idax organizes by what you're trying to do:
ida::segment,ida::function,ida::instruction,ida::decompiler. -
Full words, always.
addressnotea.removenotdel.commentnotcmt.operandnotop. Every name should be legible in isolation. -
Fully opaque. No
segment_t*. Nofunc_t*. Noinsn_t. No.raw()escape hatch. SDK types never appear in any public header. Value objects are snapshots, not live proxies into mutable kernel state. -
Uniform errors. Every fallible operation returns
ida::Result<T>orida::Status(aliases forstd::expected<T, ida::Error>). No more checking forBADADDR,-1,false,0, ornullptrdepending on which SDK function you called. -
Safe by default. RAII subscriptions. Value semantics. No manual lock/unlock. No pointer lifetime hazards in the public API.
-
Progressive disclosure. Simple operations are simple. Advanced options are available as structured parameters, never as obscure flag bitmasks.
idax spans the SDK surface across core analysis, module-authoring, and interactive workflows. 41 public headers cover 38 concept domains plus the umbrella, shared core, and error headers:
| Domain | Namespace | What it wraps |
|---|---|---|
| Addresses | ida::address |
Predicates, item traversal, range iteration, predicate search |
| Byte access | ida::data |
Read/write/patch bytes, typed values, checked fixed-width arrays through 512 bits, processor-aware tbyte/packed-real definitions, owned custom type/format registration and creation, configurable copied string-list inventory, binary pattern search |
| Database | ida::database |
Open/save/close, metadata, snapshots, file/memory transfer |
| Paths | ida::path |
Portable basename/dirname/directory helpers for plugin workflows |
| Segments | ida::segment |
CRUD, properties, permissions, iteration |
| Functions | ida::function |
CRUD, chunks, frames, register variables, callers/callees, prototype export/apply |
| Instructions | ida::instruction |
Decode/create, operand access/read-write metadata, representation controls, xref conveniences |
| Names | ida::name |
Set/get/force/remove, filtered copied inventories, address-based or arbitrary-symbol demangling, resolution, properties |
| Cross-refs | ida::xref |
Unified reference model, typed code/data refs, add/remove/enumerate |
| Offset references | ida::offset |
Opaque operand reference formats/options, copied metadata, rendering, calculation, verified mutation, and reference-aware data xrefs |
| Comments | ida::comment |
Regular/repeatable, anterior/posterior lines, bulk operations, rendering |
| Types | ida::type |
Type construction, structs/unions/members, apply/retrieve, bulk declaration import/rendering, dependency-ordered declarations, type graph rendering, type libraries |
| Source parsers | ida::parser |
Third-party parser selection by name/language, arguments/options, source/file ingestion, and parse reports |
| IDC scripts | ida::script |
Opaque values, exact access and explicit coercion, objects/slices, evaluation, compilation, calls, globals/references, and script/include resolution |
| Directory trees | ida::directory |
All eight built-in organization trees, copied entries, directory/item mutation, ordering, search, and partial bulk reports |
| Persistent registry | ida::registry |
Scoped plugin configuration with typed string/binary/integer/boolean values, copied key/value inventories, ordered string lists, and deletion |
| Register tracking | ida::registers |
Named backward value tracking, copied constant/stack-relative candidates and origins, nearest-of-two selection, and semantic cache invalidation |
| Entries | ida::entry |
Entry point enumeration, add/rename/forwarder workflows |
| Fixups | ida::fixup |
Fixup descriptors, traversal, custom fixup handlers |
| Search | ida::search |
Text (with regex), immediate, binary pattern, structural search |
| Analysis | ida::analysis |
Auto-analysis control, scheduling, waiting |
| Bookmarks | ida::bookmark |
Opaque address bookmarks with copied descriptions, deterministic slots, lookup, update, enumeration, and identity-preserving removal |
| Navigation history | ida::navigation |
Persistent opaque address histories with semantic channels, copied metadata, cursor movement, exact replacement, and channel transfer |
| Problems | ida::problem |
Typed analysis-problem recording, descriptions, ordered lookup, names, removal, and presence |
| Exception regions | ida::exception |
Opaque fragmented C++/SEH protected regions, handlers, membership, mutation, and system-EH lookup |
| Lumina | ida::lumina |
Lumina metadata pull/push wrappers and connection checks |
| Undo | ida::undo |
Opaque named restore points, copied next-action labels, and undo/redo execution |
| Events | ida::event |
Typed IDB subscriptions, generic filtering/routing, RAII guards |
| Plugins | ida::plugin |
Plugin base class, owned action registration/activation, scoped one-call hotkeys, menu/toolbar/popup attach+detach, context callbacks with optional Local Types refs |
| Loaders | ida::loader |
Loader base class, InputFile abstraction, typed request/flag models, registration macro |
| Processors | ida::processor |
Loadable LPH/procmod_t bridge, exact processor flags, typed instruction/operand analysis, tokenized output, switch detection |
| Debugger | ida::debugger |
Process lifecycle, breakpoints, memory, registers, typed event subscriptions |
| Decompiler | ida::decompiler |
Scoped Hex-Rays ownership, decompile, pseudocode, variables, ctree visitor, lvar metadata, semantic persisted-comment positions/enumeration, pseudocode-switch/popup events, address mapping |
| Lines | ida::lines |
Tagged text/color helpers plus copied half-open source-file address mappings |
| UI | ida::ui |
Messages, dialogs/forms including typed ask_form and fixed-shape binding entrypoints, optional Qt clipboard helpers (IDAX_ENABLE_QT_CLIPBOARD with IDA-compatible QT_NAMESPACE=QT Qt), wait-box progress UI, stable widget/current-widget and custom-viewer APIs, choosers, timers, UI/VIEW event subscriptions |
| Graphs | ida::graph |
Graph objects, node/edge CRUD, flow charts, basic blocks, switch-table metadata |
| Storage | ida::storage |
Netnode abstraction, alt/sup/hash/blob operations |
Plus cross-cutting primitives: ida::Error, ida::Result<T>, ida::Status, shared option structs, diagnostics, and logging.
Real-world port parity notes are tracked in
docs/port_gap_audit_examples.md, with the
current ida-cdump migration checklist in
docs/codedump_migration_checklist.md.
Every fallible operation in idax uses one return type:
namespace ida {
template <typename T>
using Result = std::expected<T, Error>; // value or error
using Status = std::expected<void, Error>; // success or error
struct Error {
ErrorCategory category; // Validation, NotFound, Conflict, Unsupported, SdkFailure, Internal
int code;
std::string message;
std::string context;
};
}No more guessing. No more checking the docs to figure out if false means failure or "not found" or "empty" or "already exists":
// Before (raw SDK):
ea_t ea = get_name_ea(BADADDR, "main");
if (ea == BADADDR) { /* failure? doesn't exist? wrong arguments? */ }
// After (idax):
auto ea = ida::name::resolve("main");
if (!ea) {
// ea.error().category tells you exactly what happened
// ea.error().message tells you why
}// Iterate every segment
for (auto seg : ida::segment::all()) {
std::cout << seg.name() << " "
<< std::hex << seg.start() << "-" << seg.end()
<< " bits=" << seg.bitness()
<< " rwx=" << seg.permissions().read
<< seg.permissions().write
<< seg.permissions().execute << "\n";
}
// Find a function, inspect its frame
auto fn = ida::function::at(0x401000);
if (fn) {
auto frame = ida::function::frame(fn->start());
if (frame)
std::cout << "locals: " << frame->local_variables_size()
<< " args: " << frame->arguments_size() << "\n";
}auto insn = ida::instruction::decode(address);
if (insn) {
std::cout << insn->mnemonic() << " (" << insn->size() << " bytes)\n";
for (size_t i = 0; i < insn->operand_count(); ++i) {
auto op = insn->operand(i);
if (op && op->is_register())
std::cout << " reg: " << op->register_name()
<< " read=" << op->is_read()
<< " written=" << op->is_written() << "\n";
}
// Change operand display format
ida::instruction::set_operand_hex(address, 1);
}if (auto avail = ida::decompiler::available(); avail && *avail) {
auto df = ida::decompiler::decompile(function_address);
if (df) {
// Get pseudocode
std::cout << *df->pseudocode() << "\n";
// Enumerate local variables
auto variables = df->variables();
if (variables) {
for (const auto& var : *variables)
std::cout << var.index << ": " << var.name
<< " : " << var.type_name << "\n";
}
// Walk the ctree.
ida::decompiler::for_each_expression(*df,
[](ida::decompiler::ExpressionView expr) {
if (expr.type() == ida::decompiler::ItemType::ExprHelper) {
auto helper = expr.helper_name();
if (helper)
std::cout << *helper << "\n";
}
return ida::decompiler::VisitAction::Continue;
});
}
auto refs = ida::decompiler::collect_referenced_types(function_address);
if (refs)
std::cout << "referenced type ordinals: "
<< refs->ordinals.size() << "\n";
}// Subscribe to owned mutation snapshots; callback-side unsubscribe is supported.
auto token = ida::event::on_segment_moved(
[](const ida::event::SegmentMovedEvent& event) {
ida::ui::message("segment moved to " + std::to_string(event.to) + "\n");
});
ida::event::ScopedSubscription guard(*token); // RAII: unsubscribes in destructorauto lo = *ida::database::min_address();
auto hi = *ida::database::max_address();
// Find an ELF signature
auto hit = ida::data::find_binary_pattern(lo, hi, "7F 45 4C 46");
if (hit) {
// Patch it
ida::data::patch_byte(*hit, 0x00);
// Read it back to confirm
auto val = ida::data::read_byte(*hit);
assert(val && *val == 0x00);
}// Build types programmatically
auto int_t = ida::type::TypeInfo::int32();
auto ptr_t = ida::type::TypeInfo::pointer_to(int_t);
auto arr_t = ida::type::TypeInfo::array_of(int_t, 16);
// Create a struct
auto st = ida::type::TypeInfo::create_struct();
st.add_member("flags", ida::type::TypeInfo::uint32());
st.add_member("buffer", ida::type::TypeInfo::array_of(ida::type::TypeInfo::uint8(), 256));
st.save_as("packet_header");
// Preserve exact IDA shifted-pointer metadata without exposing ptr_type_data_t.
auto packet = ida::type::TypeInfo::by_name("packet_header");
if (packet) {
auto shifted = ida::type::TypeInfo::pointer_to(*packet)
.with_shifted_parent(*packet, 8);
if (shifted) {
auto details = shifted->pointer_details(); // parent + signed byte delta
}
}
// Replace only an exact local forward declaration, preserving its ordinal.
auto forward = ida::type::TypeInfo::by_name("packet_forward");
if (forward && forward->is_forward_declaration()
&& forward->forward_declaration_kind() == ida::type::TypeKind::Struct) {
auto complete = ida::type::TypeInfo::create_struct();
complete.add_member("length", ida::type::TypeInfo::uint32(), 0);
auto replaced = complete.replace_forward_declaration("packet_forward");
}
// Keep UDT-member identities opaque while adding persistent informational
// references from exact instruction item heads.
if (packet) {
auto added = packet->ensure_member_reference(0, instruction_address);
auto sources = packet->member_references(0);
}
// Parse from C declaration
auto parsed = ida::type::TypeInfo::from_declaration("int (*callback)(void*, size_t)");idax provides base classes and registration macros for all three module types.
#include <ida/idax.hpp>
class MyPlugin final : public ida::plugin::Plugin {
public:
Info info() const override {
return {"My Plugin", "Ctrl-Alt-M", "Does something useful", "Help text"};
}
ida::Status run(size_t) override {
ida::ui::message("Plugin executed\n");
return ida::ok();
}
};#include <ida/idax.hpp>
class MyLoader final : public ida::loader::Loader {
public:
ida::Result<std::optional<ida::loader::AcceptResult>>
accept(ida::loader::InputFile& file) override {
auto magic = file.read_bytes_at(0, 4);
if (!magic || magic->size() < 4) return std::nullopt;
if ((*magic)[0] == 0x7F && (*magic)[1] == 'E' &&
(*magic)[2] == 'L' && (*magic)[3] == 'F')
return ida::loader::AcceptResult{"My ELF Loader", "metapc", 1};
return std::nullopt;
}
ida::Status load(ida::loader::InputFile& file, std::string_view) override {
auto r = ida::loader::set_processor("metapc");
if (!r) return r;
return ida::loader::file_to_database(file.handle(), 0, 0x400000, 0x1000, true);
}
};
IDAX_LOADER(MyLoader)#include <ida/idax.hpp>
class MyProcessor final : public ida::processor::Processor {
public:
ida::processor::ProcessorInfo info() const override {
ida::processor::ProcessorInfo pi;
pi.id = 0x8001;
pi.short_names = {"myproc"};
pi.long_names = {"My Custom Processor"};
pi.default_bitness = 32;
pi.registers = {{"r0", false}, {"r1", false}, {"sp", false}, {"pc", false},
{"cs", false}, {"ds", false}};
pi.first_segment_register = 4;
pi.last_segment_register = 5;
pi.code_segment_register = 4;
pi.data_segment_register = 5;
pi.instructions = {{"nop", 0}, {"mov", 0}, {"add", 0}, {"jmp", 0}};
return pi;
}
ida::Result<int> analyze(ida::Address address) override {
// Decode instruction at address, return instruction length
return 4;
}
ida::Result<ida::processor::AnalyzeDetails>
analyze_with_details(ida::Address address) override {
ida::processor::AnalyzeDetails details;
details.instruction_code = 0;
details.size = 4;
return details;
}
ida::processor::EmulateResult emulate(ida::Address) override {
return ida::processor::EmulateResult::Success;
}
void output_instruction(ida::Address) override {}
ida::processor::OutputInstructionResult
output_instruction_with_context(ida::Address,
ida::processor::OutputContext& output) override {
output.mnemonic("nop");
return ida::processor::OutputInstructionResult::Success;
}
ida::processor::OutputOperandResult output_operand(ida::Address, int) override {
return ida::processor::OutputOperandResult::Hidden;
}
};
IDAX_PROCESSOR(MyProcessor)idax normalizes every SDK pattern. Here's a sampling of the mapping:
| Raw SDK | idax | What changed |
|---|---|---|
getseg(ea) |
ida::segment::at(address) |
Full word, returns Result<Segment> |
get_func(ea) |
ida::function::at(address) |
Opaque value object, no raw func_t* |
decode_insn(&insn, ea) |
ida::instruction::decode(address) |
No output parameter, returns Result<Instruction> |
get_byte(ea) |
ida::data::read_byte(address) |
Verb-first, explicit read intent |
put_byte(ea, val) |
ida::data::write_byte(address, value) |
Explicit write intent |
patch_byte(ea, val) |
ida::data::patch_byte(address, value) |
Same concept, consistent namespace |
set_name(ea, n, SN_NOWARN) |
ida::name::set(address, name) |
No flag bitmasks |
force_name(ea, n, SN_...) |
ida::name::force_set(address, name) |
Explicit "force" semantics |
set_cmt(ea, txt, rpt) |
ida::comment::set(address, text, repeatable) |
Full word, typed boolean |
add_cref(from, to, fl_CN) |
ida::xref::add_code_ref(from, to, type) |
Typed CodeType enum |
find_text(...) |
ida::search::text(query, start, options) |
Structured options, no flag bitmasks |
auto_wait() |
ida::analysis::wait() |
Self-explanatory |
del_func(ea) |
ida::function::remove(address) |
remove not del |
get_segm_qty() |
ida::segment::count() |
count not qty |
The full migration map is in docs/migration/legacy_to_wrapper.md.
idax is built as a hybrid library: a static archive (libidax.a) plus public headers.
include/ida/
idax.hpp # Master include
error.hpp # Error model (Result<T>, Status, Error)
core.hpp # Shared option structs
diagnostics.hpp # Logging and counters
address.hpp ... ui.hpp # 20+ domain headers
src/
*.cpp # Compiled adapters (SDK calls happen here)
detail/
sdk_bridge.hpp # Private SDK include bridge (never public)
type_impl.hpp # TypeInfo pimpl internals
tests/
unit/ # Pure logic tests (no IDA runtime)
integration/ # idalib-based tests with real binaries
fixtures/ # Test binaries and pre-analysed databases
examples/
plugin/ # Plugin examples (quickstart + ports including abyss/idapcode/Diaphora/Symless)
loader/ # Custom ELF loader example
procmod/ # Custom processor module example
full/ # Real-world full ports (e.g. JBC)
tools/ # idalib-style tool ports and scaffolds
Key architectural decisions:
-
Opaque boundary. The
detail/sdk_bridge.hppheader is the single point where SDK headers are included. Public headers never#includeany SDK file. Internalfriend struct XxxAccesspatterns allow implementation files to populate opaque value objects. -
SDK-agnostic linkage.
libidax.acompiles against SDK headers but does not force a link target. Consumers bring their ownidasdk::plugin,idasdk::idalib, or whatever target matches their use case. -
Value semantics everywhere.
Segment,Function,Instruction,Operand,TypeInfo--- all are value objects that snapshot SDK state at construction time. They don't hold raw pointers into mutable kernel data structures. -
Pimpl for heavy types.
TypeInfouses a pimpl pattern (viadetail/type_impl.hpp) to hide the SDK'stinfo_twhile allowing full type manipulation.DecompiledFunctionholds a reference-countedcfuncptr_tinternally and is move-only.
- CMake 3.27+
- C++23-capable compiler (Clang 17+, GCC 14+, MSVC 2022 17.10+)
- IDA SDK with
ida-cmakebootstrap IDASDKenvironment variable pointing to the SDK root
export IDASDK=/path/to/ida-sdk
cmake -B build -DIDAX_BUILD_TESTS=ON -DIDAX_BUILD_EXAMPLES=ON
cmake --build buildFull integration coverage requires a real IDA installation (the idalib runtime).
Set IDADIR to your IDA install path, or let CMake auto-discover it:
ctest --test-dir build --output-on-failureThe test suite includes 33 targets: 8 unit/compile-surface tests and 25 integration tests covering every namespace with a real ELF64 fixture binary.
For repeatable OS/compiler/profile runs, use scripts/run_validation_matrix.sh and
track evidence in docs/compatibility_matrix.md.
cmake --install build --prefix /path/to/installfind_package(idax REQUIRED)
target_link_libraries(my_plugin PRIVATE idax::idax idasdk::plugin)Or for idalib-based tools:
find_package(idax REQUIRED)
target_link_libraries(my_tool PRIVATE idax::idax idasdk::idalib)cpack --config build/CPackConfig.cmake -B build
# Produces idax-0.1.0-Darwin.tar.gz (or equivalent for your platform)idax is validated through layered testing:
| Layer | What it tests | Runtime needed |
|---|---|---|
| Unit tests | Error model, diagnostics, range semantics, iterator contracts | None |
| API surface parity | Compile-only check that all 38 domain namespaces and types exist | None |
| Smoke test | 267 checks across every established namespace, end-to-end | idalib + fixture |
| Domain integration | Dedicated suites: types, fixups, operands, decompiler, events, etc. | idalib + fixture |
| Scenario tests | Loader/processor module lifecycle and callback wiring | idalib + fixture |
Current status: 38/38 test targets passing (267 smoke checks + 37 focused/unit suites).
| Document | Description |
|---|---|
docs/tutorial/first_contact.md |
5-step beginner walkthrough |
docs/tutorial/function_discovery_events.md |
Event-hook tutorial for new-function discovery processing |
docs/tutorial/rust_plugin_refs_to.md |
Rust plugin-action pattern for incoming-xref analysis with refs_to |
docs/tutorial/call_graph_traversal.md |
Transitive caller traversal with cycle guards and depth limits |
docs/tutorial/multi_binary_signature_generation.md |
Advanced multi-binary signature-generation pipeline tutorial |
docs/tutorial/distributed_analysis_consistency.md |
Distributed-analysis consistency model and merge architecture |
docs/tutorial/safety_performance_tradeoffs.md |
idax-wrapper vs raw-SDK trade-offs and inconsistent-state recovery playbook |
docs/surface_selection_guide.md |
API-surface decision guide (C++ wrapper vs safe Rust vs raw FFI) |
docs/quickstart/plugin.md |
Plugin action registration |
docs/quickstart/loader.md |
Custom loader skeleton |
docs/quickstart/processor.md |
Processor module skeleton |
docs/cookbook/common_tasks.md |
Rename, comment, search, patch recipes |
docs/cookbook/disassembly_workflows.md |
Decode, operand, xref recipes |
docs/migration/legacy_to_wrapper.md |
SDK-to-idax function mapping |
docs/api_reference.md |
Complete header index |
docs/compatibility_matrix.md |
OS/compiler validation coverage |
docs/namespace_topology.md |
Namespace map and type inventory |
docs/storage_migration_caveats.md |
Netnode migration safety notes |
docs/port_gap_audit_examples.md |
Consolidated real-world example port gap findings |
docs/codedump_migration_checklist.md |
ida-cdump gap-to-idax migration checklist |
MIT