Rust client for the Ghidra decompiler. Two backends, one crate, one import:
live(default) — HTTP/RPC client. Lights up when a Ghidra Desktop with theLibGhidraHostextension is reachable, or when you spawn a headless project session vialaunch_headless_project.local— offline backend. Links the C++ libghidra engine + its embedded Sleigh specs via a cxx FFI bridge. No Ghidra install required at runtime. Mirrorspython/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 onlyThe 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.
[dependencies]
libghidra = { git = "https://github.com/0xeb/libghidra" }That's it. Links against ureq + prost; builds in seconds.
The local feature pulls in the cxx FFI bridge into
libghidra::local_whole. Two install paths:
-
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 -
Build from source — if no prebuilt archive matches your target, point
build.rsat 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. Stage the flat SDK layout expected by build.rs. Keep the vendored # Protobuf/Abseil archives with the two libghidra archives so their # versions cannot drift at link time. SDK_BUNDLE=$PWD/sdk-bundle mkdir -p "$SDK_BUNDLE/include" "$SDK_BUNDLE/lib" cp -R cpp/include/* "$SDK_BUNDLE/include/" find build -type f \( \ -name 'liblibghidra_*.a' -o -name 'libghidra_*.lib' -o \ -name 'liblibghidra_*.lib' -o -name 'libprotobuf.a' -o \ -name 'libprotobuf.lib' -o -name 'libabsl_*.a' -o \ -name 'absl_*.lib' -o -name 'libutf8_*.a' -o \ -name 'utf8_*.lib' \ \) -exec cp {} "$SDK_BUNDLE/lib/" \; # 5. Tell cargo where it lives export LIBGHIDRA_PREBUILT_DIR=$SDK_BUNDLE cargo build --features localThe CI matrix (.github/workflows/ci.yml) does steps 1-3 automatically on every release build, which is why
cargo binstall libghidrausers 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 triple | Linux x86_64 | Linux aarch64 | macOS arm64 | Windows x86_64 |
|---|---|---|---|---|
Prebuilt cargo binstall available |
✅ | ✅ | ✅ | ✅ |
Other targets (FreeBSD, illumos, etc.) work via the source path; expect a multi-minute first compile.
Release prebuilt archives are self-contained: they carry the exact
Protobuf/Abseil/utf8 archives used to build libghidra_client, and Linux
archives also carry the matching static libbfd and its archive dependencies.
No Protobuf or binutils development package is needed to consume one.
A source build still needs the C++ prerequisites listed in the top-level
README. On Linux that includes the libbfd headers (binutils-dev on
Debian/Ubuntu, binutils-devel on Fedora/RHEL) at CMake build time. A custom
bundle must retain the fetched Protobuf/Abseil/utf8 archives as shown above;
otherwise the final Rust link is intentionally fail-closed instead of silently
mixing incompatible Protobuf versions. LIBGHIDRA_LINK_LIBS and
LIBGHIDRA_LINK_DYLIBS remain available for deliberately non-standard SDK
layouts.
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>(())Live/headless sessions can enumerate and import Ghidra project programs while keeping one active program per host. Import and analyze with Ghidra, 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>(())Call close(true) or shut down with ShutdownPolicy::Save to persist the
project. Later Rust, Python, C++, GUI, or ghidrasql sessions can reopen the same
project and select saved programs by domain path. See
examples/multi_program_strings.rs for a
variadic live example that imports multiple binaries, counts strings, saves, and
shuts headless down.
# #[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>(())See examples/ for the full set.
| Example | Mode | Coverage |
|---|---|---|
quickstart.rs |
live | Connect, list functions, decompile one |
local_quickstart.rs |
local | Open a binary offline and decompile |
format_detect.rs |
– | Identify Sleigh language ID without opening |
explore_binary.rs |
live | Memory blocks, functions, symbols, xrefs, strings |
annotate_and_export.rs |
live | Create types, rename functions, comments, batch decompile |
memory_ops.rs |
live | Memory blocks, read/write/patch bytes |
disassemble.rs |
live | Instructions and disassembly listing |
comments.rs |
live | Comment CRUD (Eol, Pre, Post, Plate, Repeatable) |
data_items.rs |
live | Apply data types, rename/delete data items |
symbols.rs |
live | Symbol query, rename, delete |
type_system.rs |
live | Type overview: structs, aliases, enums, unions |
struct_builder.rs |
live | Struct member add/rename/retype/delete |
enum_builder.rs |
live | Enum member add/rename/revalue/delete |
function_signatures.rs |
live | Signatures, parameter mutation, prototype override |
cfg_analysis.rs |
live | Basic blocks and CFG edges |
decompile_tokens.rs |
live | Pseudocode token records and local metadata |
end_to_end.rs |
live | Launch headless Ghidra, analyze, enumerate, save, shutdown |
multi_program_strings.rs |
live | Import/analyze multiple binaries, count strings per program, save project |
function_tags.rs |
live | Function tag CRUD and mappings |
parse_declarations.rs |
live | Parse C declarations into data types |
session_lifecycle.rs |
live | Status, capabilities, revision, save/discard |
structural_analysis.rs |
live | Switch tables, dominators, post-dominators, loops |
pagination.rs |
live | fetch_all and Paginator with custom page size |
For the full method-by-method reference, see the API Reference.
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.
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.
Both backends share the same record/response types in
models.rs. Method signatures match
python/src/libghidra/:
| Area | Methods |
|---|---|
| Health | get_status, get_capabilities |
| Session | open_project, close_project, list_project_files, import_program, open_program, close_program, save_program, discard_program, get_revision, shutdown (live) |
| Memory | read_bytes, write_bytes (live), patch_bytes_batch (live), list_memory_blocks |
| Functions | Function lookup/list/rename, basic blocks, CFG edges, structural analysis (live), function tags (live) |
| Symbols | get_symbol, list_symbols, rename_symbol, delete_symbol (live) |
| Xrefs | list_xrefs |
| Types | Queries everywhere; mutations on the live backend |
| Decompiler | get_decompilation, list_decompilations |
| Listing | Instructions, 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.
.--------- 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 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.
| Concern | Python | Rust |
|---|---|---|
| Install | pip install <release-url> |
git dep on this repo (live), cargo binstall libghidra (local) |
| Live backend | GhidraClient (HTTP, requests) |
GhidraClient (HTTP, ureq) |
| Local backend | LocalClient (nanobind → C++) |
LocalClient (cxx → C++) |
| Sleigh specs | embedded in _libghidra.pyd/.so |
embedded in the prebuilt archive |
| Format detection | libghidra.format_detect |
libghidra::format_detect |
| Examples | python/examples/ (25 scripts) |
rust/examples/ (23 scripts) |
If you find behaviour that diverges between the two languages for the same backend, please open an issue — that's a bug.