libghidra Rust Client

May 5, 2026 · View on GitHub

Rust client for the Ghidra decompiler. Two backends, one crate, one import:

  • live (default) — HTTP/RPC client. Lights up when a Ghidra Desktop with the LibGhidraHost extension is reachable, or when you spawn a headless instance via launch_headless.
  • local — offline backend. Links the C++ libghidra engine + its embedded Sleigh specs via a cxx FFI bridge. No Ghidra install required at runtime. Mirrors python/src/libghidra/local.py.
# Cargo.toml
[dependencies]
libghidra = { git = "https://github.com/0xeb/libghidra" }                                # live only
libghidra = { git = "https://github.com/0xeb/libghidra", features = ["local"] }          # live + local
libghidra = { git = "https://github.com/0xeb/libghidra", default-features = false, features = ["local"] }
                                                                                          # local only

The crate is not published to crates.io — same distribution model as the Python wheel (which isn't on PyPI either). Pull from the GitHub repo directly, or use cargo binstall against the Releases page for a prebuilt local-mode archive. The two distributions are kept deliberately in sync method-for-method.

Install

Live (HTTP) backend — pure Rust, no system deps

[dependencies]
libghidra = { git = "https://github.com/0xeb/libghidra" }

That's it. Links against ureq + prost; builds in seconds.

Local (offline) backend — needs the C++ engine

The local feature pulls in the cxx FFI bridge into libghidra::local_whole. Two install paths:

  1. cargo-binstall (recommended) — fetches a prebuilt archive for your target from the GitHub Release matching the crate version. No C++ build on your machine.

    cargo binstall libghidra
    
  2. Build from source — if no prebuilt archive matches your target, point build.rs at a libghidra C++ SDK:

    # 1. Apply libghidra's patches to your Ghidra source tree. Skipping
    #    this is the #1 footgun: some local/offline loads will fail or
    #    mis-detect architecture metadata.
    cd <path/to/ghidra>
    for p in <path/to/libghidra>/cpp/patches/*.patch; do patch -p1 < "$p"; done
    
    # 2. Overlay compiled .sla Sleigh grammars from the matching Ghidra
    #    release ZIP into the source tree (the git tree only has .slaspec
    #    sources). Without .sla files the decompiler emits halt_baddata()
    #    for every function. See ci.yml's "Overlay compiled Sleigh .sla
    #    grammars" step for the exact rsync invocation.
    
    # 3. Build the SDK
    cd <path/to/libghidra>
    cmake -S cpp -B build -DLIBGHIDRA_WITH_LOCAL=ON \
                           -DGHIDRA_SOURCE_DIR=<path/to/ghidra>
    cmake --build build --config Release
    
    # 4. Tell cargo where it lives
    export LIBGHIDRA_PREBUILT_DIR=$PWD/sdk-bundle  # or LIBGHIDRA_INCLUDE_DIR + LIBGHIDRA_LIB_DIR
    cargo build --features local
    

    The CI matrix (.github/workflows/ci.yml) does steps 1-3 automatically on every release build, which is why cargo binstall libghidra users never have to think about patches or .sla overlays. The full build docs live in the top-level README's "Building the C++ SDK" section.

Supported targets for prebuilt archives (matches the Python wheel matrix):

Target tripleLinux x86_64Linux aarch64macOS arm64Windows x86_64
Prebuilt cargo binstall available

Other targets (FreeBSD, illumos, etc.) work via the source path; expect a multi-minute first compile.

System prereqs for the local feature

The libghidra C++ engine depends on a few system libraries that the prebuilt archive does not bundle (it ships static .a archives for the libghidra targets only — protobuf, zlib, bfd come from the host):

  • Linux (Debian / Ubuntu / Raspberry Pi OS):
    sudo apt-get install libprotobuf-dev binutils-dev zlib1g-dev
    
    binutils-dev provides libbfd (used by the offline loader); libprotobuf-dev provides libprotobuf-lite. Tested on Debian 13 trixie aarch64 (Raspberry Pi 5).
  • Linux (RHEL / Fedora / CentOS):
    sudo dnf install protobuf-devel binutils-devel zlib-devel
    
  • macOS (Homebrew): the local feature builds against Homebrew-installed protobuf + zlib. Set LIBGHIDRA_EXTRA_LIB_PATHS=/opt/homebrew/lib (Apple Silicon) or /usr/local/lib (Intel) before cargo build.
  • Windows: prebuilt archive bundles everything it needs; no extra deps to install.

If cargo build --features local fails with "could not find native static library protobuf-lite" (or similar), install the missing -dev package and re-run. Override the link list entirely with LIBGHIDRA_LINK_LIBS=static=libghidra_local,... if you have a non- standard layout.

Known: libbfd ABI mismatch on newer Linux distros

The current prebuilt archives are built in a manylinux_2_28 container where binutils-devel provides libbfd ≈ 2.30. When you link those archives against libbfd 2.44+ (Debian 13 trixie, Ubuntu 24.04, Fedora 40+), read_bytes and the decompiler will read garbage from the loaded image — same archive, same bridge, but BFD's internal struct layouts shifted between major versions and the ABI is silent.

Symptom: local_quickstart reports the correct language ID (AARCH64:LE:64:v8A etc.) and open_program succeeds, but read_bytes returns process-memory garbage and the decompiler emits halt_baddata() warnings.

Workaround until prebuilds bundle a matched libbfd (see issue tracker for the open ticket): build the C++ SDK on the same machine you're running Rust on, and point LIBGHIDRA_PREBUILT_DIR at it instead of the downloaded archive. The Python wheel sidesteps this by statically embedding libbfd via auditwheel; the Rust archive is intentionally leaner and doesn't yet do the same.

Quick start — live (HTTP)

use libghidra as ghidra;

let client = ghidra::connect("http://127.0.0.1:18080");

let status = client.get_status()?;
println!("{} v{}", status.service_name, status.service_version);

let funcs = client.list_functions(0, u64::MAX, 10, 0)?;
for f in &funcs.functions {
    println!("0x{:x}  {}", f.entry_address, f.name);
}
# Ok::<(), libghidra::Error>(())

Project files and switching

Live/headless sessions can enumerate and import Ghidra project programs while keeping one active program per host. List project programs, close the current program, then open the next Ghidra domain path:

use libghidra::{
    ListProjectFilesRequest, OpenProgramRequest, ShutdownPolicy,
};

let files = client.list_project_files(ListProjectFilesRequest {
    programs_only: true,
    ..Default::default()
})?;
for file in &files.files {
    println!("{}", file.path);
}

client.close_program(ShutdownPolicy::Save)?;
client.open_program(OpenProgramRequest {
    project_path: "C:/work/projects".into(),
    project_name: "firmware".into(),
    program_path: "/payload.elf".into(),
    ..Default::default()
})?;
# Ok::<(), libghidra::Error>(())

Quick start — local (offline)

# #[cfg(feature = "local")] {
use libghidra::format_detect::detect_and_open;
use libghidra::{local_with, LocalClientOptions};

// Auto-detect the Sleigh language ID from the binary headers.
let client = local_with(LocalClientOptions::auto())?;
let detected = detect_and_open(&client, "/usr/bin/ls", None)?;
println!("language = {}", detected.language_id);

let dec = client
    .get_decompilation(/*addr=*/0xa000, /*timeout_ms=*/30_000)?
    .decompilation
    .expect("no decompilation");
println!("{}", dec.pseudocode);
# }
# Ok::<(), libghidra::Error>(())

Examples

See examples/ for the full set.

ExampleModeCoverage
quickstart.rsliveConnect, list functions, decompile one
local_quickstart.rslocalOpen a binary offline and decompile
format_detect.rsIdentify Sleigh language ID without opening
explore_binary.rsliveMemory blocks, functions, symbols, xrefs, strings
annotate_and_export.rsliveCreate types, rename functions, comments, batch decompile
memory_ops.rsliveMemory blocks, read/write/patch bytes
disassemble.rsliveInstructions and disassembly listing
comments.rsliveComment CRUD (Eol, Pre, Post, Plate, Repeatable)
data_items.rsliveApply data types, rename/delete data items
symbols.rsliveSymbol query, rename, delete
type_system.rsliveType overview: structs, aliases, enums, unions
struct_builder.rsliveStruct member add/rename/retype/delete
enum_builder.rsliveEnum member add/rename/revalue/delete
function_signatures.rsliveSignatures, parameter mutation, prototype override
cfg_analysis.rsliveBasic blocks and CFG edges
decompile_tokens.rslivePseudocode token records and local metadata
end_to_end.rsliveLaunch headless Ghidra, analyze, enumerate, save, shutdown
function_tags.rsliveFunction tag CRUD and mappings
parse_declarations.rsliveParse C declarations into data types
session_lifecycle.rsliveStatus, capabilities, revision, save/discard
structural_analysis.rsliveSwitch tables, dominators, post-dominators, loops
pagination.rslivefetch_all and Paginator with custom page size

For the full method-by-method reference, see the API Reference.

Format detection

libghidra::format_detect is a pure-Rust port of the Python format_detect module. PE / ELF / Mach-O / fat Mach-O headers map to a Sleigh language ID without spawning the C++ engine:

use libghidra::format_detect::detect;

let detected = detect("/usr/bin/ls")?;
assert_eq!(detected.language_id, "x86:LE:64:default");
# Ok::<(), libghidra::Error>(())

detect_and_open(&client, path, compiler_override) is the convenience wrapper used by the local quickstart example. It works with both LocalClient and GhidraClient via the OpenProgram trait.

Pagination (live)

use libghidra::paginate::fetch_all;

# fn doit(client: libghidra::GhidraClient) -> libghidra::Result<()> {
let all_funcs = fetch_all(|limit, offset| {
    let resp = client.list_functions(0, u64::MAX, limit, offset)?;
    Ok(resp.functions)
})?;
# Ok(()) }

See examples/pagination.rs for Paginator with custom page sizes.

API surface

Both backends share the same record/response types in models.rs. Method signatures match python/src/libghidra/:

AreaMethods
Healthget_status, get_capabilities
Sessionopen_program, close_program, save_program, discard_program, get_revision, shutdown (live)
Memoryread_bytes, write_bytes (live), patch_bytes_batch (live), list_memory_blocks
FunctionsFunction lookup/list/rename, basic blocks, CFG edges, structural analysis (live), function tags (live)
Symbolsget_symbol, list_symbols, rename_symbol, delete_symbol (live)
Xrefslist_xrefs
TypesQueries everywhere; mutations on the live backend
Decompilerget_decompilation, list_decompilations
ListingInstructions, defined strings, comments (live), data items (live), bookmarks (live), breakpoints (live)

LocalClient covers the same 25 methods as Python's local.py. The remaining ~40 methods (mutations + listing extras) are live-only because the local backend is read-mostly by design — see cpp/README.md's "Out of scope" section.

Architecture

                                          .--------- live -----------.
                                          |                          |
            ghidra::connect(url)  --->  GhidraClient (ureq, prost)   |
                                                                     |
            ghidra::local()      --->  LocalClient (cxx, JSON)        | shared models, error
                                          |
                                          '--- libghidra::local_whole (C++ static archive)
                                                  |
                                                  '-- 376 embedded Sleigh specs

The cxx bridge in src/local_ffi.rs + cpp/bindings/rust_bridge.cpp emits each method's payload as a JSON string; the wrapper in src/local.rs deserializes into the same record types the live backend uses. Same record shape both sides — no fork, no duplication.

Protobuf codegen (live only)

Protobuf stubs are auto-regenerated at build time via build.rs using prost-build. Set the PROTOC env var to point to a protoc binary to enable auto-regeneration. If protoc is not available, the build falls back to the pre-generated stubs in generated/libghidra.rs.

The C++ SDK in cpp/ and the proto contracts are the reference implementations.

Comparison with the Python package

ConcernPythonRust
Installpip install <release-url>git dep on this repo (live), cargo binstall libghidra (local)
Live backendGhidraClient (HTTP, requests)GhidraClient (HTTP, ureq)
Local backendLocalClient (nanobind → C++)LocalClient (cxx → C++)
Sleigh specsembedded in _libghidra.pyd/.soembedded in the prebuilt archive
Format detectionlibghidra.format_detectlibghidra::format_detect
Examplespython/examples/ (21 scripts)rust/examples/ (22 scripts)

If you find behaviour that diverges between the two languages for the same backend, please open an issue — that's a bug.