Swift bindings - #6
Conversation
`build*/` was unanchored, so it matched at any depth — and because macOS mounts a case-insensitive filesystem, it also matched BuildXCFramework/. The SPM command plugin under bindings/swift/Plugins/ therefore could never be committed, while Package.swift referenced it, so a clean clone failed to build the Swift package. Anchoring to /build*/ keeps every root-level build directory ignored; the separate unanchored `build/` rule above still covers nested ones such as bindings/node/build/.
Walking a ctree previously meant visiting it; there was no way to ask a statement for its own parts. Adds condition/then_branch/else_branch/body/ init_expression/step_expression/expression, block_size/block_statement and switch_case_count/values/body, so a consumer can descend a decompiled function structurally instead of pattern-matching a flat visit stream. LocalVariable also gains stack_offset (vd-offset for stack variables) and register_number (the microcode mreg_t for register variables; on ARM64 x<n> is 8 + 8*n). Together these let an importer map lvars back to concrete ABI locations, which a name/type-only view cannot express. Includes a segfault fix: operand navigation now gates on op_uses_x/y/z before dereferencing. For most opcodes the x/y/z slots alias other members — z aliases an int (ptrsize) — so reaching for them on a deep ctree read a non-pointer as a cexpr_t*. The existing third() guarded only cot_tern; first() and second() were unguarded. tests/integration covers the traversal.
Recovering control flow meant string-matching mnemonics per target. Adds a BranchCondition enum and Instruction::branch_condition() that maps the mnemonic to a semantic condition (equal, less-than-signed, overflow, …), so a consumer can reason about a conditional branch without knowing whether it is reading x86 jle, ARM b.le or a PowerPC bc variant.
Adds plugin::is_plugin_available() and plugin::run_plugin(). Some IDA functionality is only reachable through a bundled plugin's run() entry point — the dyld shared cache utility being the case that motivated this — and there was no way to reach it from idax without going back to the raw SDK.
save() persists in place; there was no supported way to write the analysed database to a different path, which headless tooling needs when the input should stay untouched.
decompiler exposes microcode only while a filter or optimizer callback is on the stack, and the SDK frees the mba_t as soon as that returns. Anything that wants to read a function's microcode after the fact — an IR importer, a lifter, an analysis pass — has no way in. ida::microcode::snapshot(address, maturity) runs gen_microcode, deep copies every block, instruction and operand into owned storage, then releases the SDK's mba_t. The returned FunctionSnapshot outlives the SDK object entirely and can be moved freely, so callers own nothing of the SDK. The snapshot keeps what an IR consumer needs and the pseudocode view drops: stack-frame totals, retvaridx (at MMAT_LVARS the return value lives in an lvar rather than on m_ret's operands), block predecessors and successors, nested instructions addressable by id, and the lvar table visible at that maturity. It is a separate namespace rather than more decompiler surface because the lifetime model is the opposite one — decompiler hands out borrowed views, this hands out owned data.
Analysing a macOS/iOS dyld shared cache means driving the bundled dscu plugin, whose interface is a set of run() argument codes with no typed entry points. ida::dyld_cache wraps it: is_available(), list_modules() for both the current database and a cache file on disk, load_module()/load_section(), and the region loaders (branch islands, branch mappings, GOTs, gaps, cache data). Loading is opt-in about analysis: each entry point takes a flag for whether to drain auto-analysis before returning, because a caller loading twenty modules wants to wait once at the end rather than twenty times. docs/IDA94DyldCacheAdaptation.md records what changed in IDA 9.4 — the utility's module listing and load paths moved, so the same call sequence does not work across both versions.
The extern "C" header lived under bindings/rust/idax-sys/shim/ because Rust was its only consumer. It is now shared, so it moves to bindings/c/include/ and the Rust build script points at the new location; bindgen still generates from it and nothing about the Rust build changes otherwise. The shim gains entry points for the surface added in the preceding commits: ctree sub-structure navigation, microcode snapshots, the dyld shared cache utility, plugin invocation, branch conditions, lvar stack offsets and register numbers, and database::save_to. Node and Rust wrappers follow for the database entry points. cargo test -p idax --lib: 173 passed.
An SPM package layered like the existing bindings: CIDAX exposes the shared extern "C" header as a system-library target, IDAX is the safe Swift layer over it. 30 namespace files mirror the C++ domains. The Swift layer uses typed throws (throws(IDAError)) rather than a Result type, so an error path is visible in the signature and callers can bind the concrete error without casting. Handle-owning types are classes with deinit-based release; value snapshots are structs. Nothing in the public API exposes a pointer. Building it needs libidax.a and libidax_shim.a in place first, via bindings/swift/scripts/build-libs.sh — SPM cannot drive the CMake build itself. The manifest finds the IDA runtime through $IDADIR, falling back to an installed IDA under /Applications, because anything that links (the example executable, the test bundle) needs libida/libidalib on the link line. swift test: 64 tests in 32 suites, no IDA runtime required. bindings/swift/.gitignore had its patterns written as bindings/swift/.build/ and so on. Patterns in a nested .gitignore resolve relative to that file's own directory, so each one expanded to bindings/swift/bindings/swift/... and never matched; the build directories were only ignored incidentally by the root *.a/*.o rules. Rewritten relative to the file.
|
You ok with me taking over? |
macOS only, since the package targets macOS 13+. Reuses the same IDA install and IDASDK/IDADIR resolution as the other jobs, then builds the static libraries through bindings/swift/scripts/build-libs.sh before swift build and swift test — SPM cannot drive the CMake build itself, so the .a files have to exist first. Verified locally by running the same command sequence against SDK 9.4 on macOS arm64: 64 tests in 32 suites.
I don't mind, please feel free to change it. |
|
This PR is so big and so unrelated to the goal of IDAX (c++ wrapping of IDA's API) - are you sure you would like to take on this PR? |
@matan7890 I haven't had the time to more closely review this, so I can't really judge yet. That being said, there are some very valid additions. The only thing that really bothers me is that the LLM seemingly entirely ignored the AGENTS.md file (which usually is the case with Claude), resulting in no documentation of progress, assumptions, decisions and otherwise relevant process specification. I'd probably split this up into salvageable, semantically isolated chunks of expansions that stack on each other. The actual Swift bindings are rather isolated. |
|
@Mx-Iris Why did you delete this? It was genuinely good! |
|
Sorry, that wasn't deliberate — I didn't delete anything. I renamed the branch on my fork so it wouldn't be confused with my own working Branch is back and I've reopened it — same 10 commits, same 71 files, nothing |
|
Thanks! I'll get to it as soon as possible! |
Draft for #5.
Nine commits, each one builds on its own. The Swift package is the last; everything before it is groundwork it needs.
Two things here you might want regardless of how you feel about the Swift part:
ctree operand navigation segfaults.
first()andsecond()derefx/ywithout checkingop_uses_x/op_uses_y. Those slots alias other members for most opcodes —zaliases anint— so on a deep ctree you end up reading a non-pointer as acexpr_t*.third()did guard, but only againstcot_tern. There's an integration test for the traversal. Take this one on its own if nothing else here appeals.build*/in .gitignore isn't anchored, so it matches at any depth, and since macOS is case-insensitive it swallowsBuildXCFramework/as well. That's why that plugin directory could never be committed whilePackage.swiftreferenced it./build*/fixes it; the separatebuild/rule still covers nested ones.The two domains
I said in #5 that the Swift layer isn't self-contained. Concretely:
ida::microcode—decompileronly hands you microcode while a filter or optimizer callback is on the stack, and the SDK frees themba_tthe moment it returns.snapshot()deep copies the whole thing and releases the SDK object, so what comes back outlives it. It's a separate namespace rather than moredecompilersurface because the lifetime story is the inverse:decompilerlends borrowed views, this hands over owned data.ida::dyld_cache— wraps dscu, whose interface isrun()argument codes.docs/IDA94DyldCacheAdaptation.mdcovers what moved in 9.4.Both are here because Swift needs them, not because I'm assuming you want them. If you'd rather they landed separately, or under different names, or not at all, say the word and I'll restructure.
Left out
Per what I said in #5: no checked-in xcframework (
swift package build-xcframeworkproduces it), no third-party dependencies, nothing from our agent ledger or planning docs. Our fork has an ArgumentParser-based CLI tool for dyld cache work — that stays on our side, soPackage.swifthere pulls in nothing.The Swift layer itself
Typed throws rather than a
Resulttype, so failure shows up in the signature. Handle owners are classes releasing indeinit, value snapshots are structs, no pointer reaches the public API.SPM can't drive the CMake build, so
bindings/swift/scripts/build-libs.shproduces the.afiles first and the manifest links them. It resolves the IDA runtime from$IDADIR, falling back to an installed IDA under/Applications, since the test bundle and the example executable both needlibida/libidalibat link time.Tested
SDK 9.4, macOS arm64, against a real IDA install:
ctest— 42/42, including all 36 integration suites (so the domains added since February are exercised, not just mine)cargo test -p idax --lib— 173 passednpm test— 277 passedswift test— 64 tests in 32 suites, no IDA runtime neededAlso built the branch from a clean clone following the README, to be sure nothing needed is missing from the tree. The parity inventory moves 40 → 42 for the two new umbrella includes, with probes for both.
There's now a
build-swiftjob in the bindings workflow too — macOS only, same IDA install and path resolution as the others. It won't run on this PR since forks can't seeHCLI_KEY, but I ran the same command sequence locally against 9.4.One gap I know about: the Node addon only got the two files it actually needed.