How to export C++ struct fields to JSON using Rust & clang

Also see the Rust version of this post: How to export C++ struct fields to JSON using Rust & clang

In our previous post How to export C++ struct fields to JSON using clang and boost::json we discussed how to use libclang in combination with boost::json to parse a C++ source file and export struct definitions to JSON.

In this post we will achieve the same goal using Rust and the clang crate.

main.rs

use clang::{Clang, Entity, EntityKind, Index};
use serde_json::{json, Value};
use std::env;
use std::fs;
use std::path::Path;

fn extract_struct_fields(entity: &Entity) -> Option<Value> {
    if entity.get_kind() != EntityKind::StructDecl {
        return None;
    }

    // Struct name (may be None for anonymous structs)
    let struct_name = entity.get_name().unwrap_or_default();

    // Collect fields with running index
    let mut index = 0usize;
    let mut fields = vec![];

    entity.visit_children(|child, _parent| {
        if child.get_kind() == EntityKind::FieldDecl {
            let field_name = child.get_name().unwrap_or_default();
            let field_type = child
                .get_type()
                .map(|t| t.get_display_name())
                .unwrap_or_default();

            fields.push(json!({
                "index": index,
                "name": field_name,
                "type": field_type,
            }));
            index += 1;
        }
        clang::EntityVisitResult::Continue
    });

    Some(json!({
        "name": struct_name,
        "fields": fields
    }))
}

fn main() {
    // --- Argument & file checks (parity with the C++ version) ---
    let mut args = env::args();
    let exe = args.next().unwrap_or_else(|| "program".into());

    let Some(filename) = args.next() else {
        eprintln!("Usage: {} <source file>", exe);
        std::process::exit(1);
    };

    let path = Path::new(&filename);
    if !path.exists() {
        eprintln!("File does not exist: {}", filename);
        std::process::exit(1);
    }

    let code = match fs::read_to_string(&path) {
        Ok(s) => s,
        Err(_) => {
            eprintln!("Error opening file: {}", filename);
            std::process::exit(1);
        }
    };

    if code.is_empty() {
        eprintln!("File is empty: {}", filename);
        std::process::exit(1);
    }

    // --- libclang setup & parse as an unsaved file named "test.cpp" ---
    let clang = Clang::new().expect("Failed to load libclang");
    let index = Index::new(&clang, /*exclude_decls_from_pch=*/ false, /*display_diagnostics=*/ false);

    let unsaved = clang::Unsaved::new("test.cpp", &code);
    // No special arguments needed (parity with CXTranslationUnit_None)
    let tu = match index.parser("test.cpp").unsaved(&[unsaved]).parse() {
        Ok(tu) => tu,
        Err(_) => {
            eprintln!("Failed to parse translation unit.");
            std::process::exit(1);
        }
    };

    // --- Walk the translation unit and gather structs ---
    let root = tu.get_entity();
    let mut structs = vec![];

    root.visit_children(|child, _parent| {
        if let Some(struct_json) = extract_struct_fields(&child) {
            structs.push(struct_json);
        }
        clang::EntityVisitResult::Continue
    });

    // --- Output JSON identical in shape to the C++ version ---
    let result = json!({ "structs": structs });
    println!("{}", serde_json::to_string(&result).unwrap());
}

Cargo.toml

[package]
name = "rust_clang_structs"
version = "0.1.0"
edition = "2021"

[dependencies]
clang = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"