Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
218d76a
ctest: parse rust modules in crates
dybucc Sep 2, 2026
54e6897
ctest: add support for filtering modules
dybucc Sep 3, 2026
60b0cdd
ctest(remove): showcase skipping items with mods
dybucc Sep 4, 2026
30cf354
ctest: fmt comments
dybucc Sep 4, 2026
c75758b
ctest: differentiate between `ident` and `path`
dybucc Sep 6, 2026
3c74044
ctest: adjust `ident` call sites with new fns
dybucc Sep 6, 2026
b6de492
ctest(maybe_remove): tweak a little something
dybucc Sep 4, 2026
da30907
ctest: tweak tests and test template to pass them
dybucc Sep 6, 2026
a15dd2f
ctest: update test templates
dybucc Sep 6, 2026
5cc36f1
ctest: fmt a string to 80 characters wide
dybucc Sep 7, 2026
56204ba
chore: wip
dybucc Sep 13, 2026
372aa56
chore: wip
dybucc Sep 14, 2026
7e16883
chore: wip
dybucc Sep 15, 2026
eec5bcf
chore: wip
dybucc Sep 15, 2026
3969d83
chore: wip
dybucc Sep 16, 2026
4fb056d
chore: wip
dybucc Sep 16, 2026
ac79172
chore: wip
dybucc Sep 16, 2026
c2573e7
chore: wip
dybucc Sep 18, 2026
c7b1d2a
chore: wip
dybucc Sep 19, 2026
c89b68e
chore: wip
dybucc Sep 19, 2026
e9c28d4
chore: wip
dybucc Sep 19, 2026
5cbef5a
chore: wip
dybucc Sep 20, 2026
add0dcb
chore: wip
dybucc Sep 20, 2026
cf666c4
chore: wip
dybucc Sep 20, 2026
1e2e440
chore: wip
dybucc Sep 20, 2026
ab44844
chore: wip
dybucc Sep 21, 2026
2463ee8
chore: wip
dybucc Sep 21, 2026
87890d9
chore: wip
dybucc Sep 21, 2026
0c419dd
chore: wip
dybucc Sep 21, 2026
4b05ed4
chore: wip
dybucc Sep 22, 2026
8ae9aaa
chore: wip
dybucc Sep 22, 2026
1bce5b4
chore: wip
dybucc Sep 23, 2026
c9507dd
chore: wip
dybucc Sep 25, 2026
3c4553d
chore: wip
dybucc Sep 25, 2026
a9b1dd6
chore: wip
dybucc Sep 25, 2026
9a86503
chore: wip
dybucc Sep 26, 2026
1b1e58e
chore: wip
dybucc Sep 27, 2026
dbccfb9
chore: wip
dybucc Sep 27, 2026
366231b
chore: wip
dybucc Sep 28, 2026
4135ca6
chore: wip
dybucc Sep 28, 2026
0114011
chore: wip
dybucc Sep 28, 2026
6c58b3c
chore: wip
dybucc Sep 28, 2026
b376b8d
chore: wip
dybucc Sep 29, 2026
68f45b5
chore: wip
dybucc Oct 1, 2026
a37e9be
chore: wip
dybucc Oct 5, 2026
4b7454b
chore: wip
dybucc Oct 5, 2026
bb13837
chore: wip
dybucc Oct 5, 2026
f3032eb
chore: wip
dybucc Oct 6, 2026
68cb6e1
chore: wip
dybucc Oct 6, 2026
35051cc
chore: wip
dybucc Oct 7, 2026
cf06ec2
chore: wip
dybucc Oct 7, 2026
eeb55b1
chore: wip
dybucc Oct 8, 2026
7fa5dbd
chore: wip
dybucc Oct 8, 2026
328fde6
chore: wip
dybucc Oct 8, 2026
e2b41f9
chore: wip
dybucc Oct 9, 2026
75de193
chore: wip
dybucc Oct 9, 2026
64021b8
chore: wip
dybucc Oct 12, 2026
a9ec090
chore: wip
dybucc Oct 12, 2026
1802430
chore: wip
dybucc Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions ctest-test/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ fn test_ctest() {
.header("t1.h")
.include("src")
.skip_private(true)
.rename_fn(|f| f.link_name().unwrap_or(f.ident()).to_string().into())
.rename_static(|s| s.link_name().unwrap_or(s.ident()).to_string().into())
.rename_fn(|f| f.link_name().unwrap_or(f.path()).to_string().into())
.rename_static(|s| s.link_name().unwrap_or(s.path()).to_string().into())
.rename_union_ty(|ty| (ty == "T1Union").then_some(ty.to_string()))
.rename_struct_ty(|ty| (ty == "Transparent").then_some(ty.to_string()))
.volatile_struct_field(|s, f| s.ident() == "V" && f.ident() == "v")
.volatile_static(|s| s.ident() == "vol_ptr")
.volatile_static(|s| s.ident() == "T1_fn_ptr_vol")
.volatile_fn_arg(|f, p| f.ident() == "T1_vol0" && p.ident() == "arg0")
.volatile_fn_arg(|f, p| f.ident() == "T1_vol2" && p.ident() == "arg1")
.volatile_fn_return_type(|f| f.ident() == "T1_vol1")
.volatile_fn_return_type(|f| f.ident() == "T1_vol2")
.volatile_struct_field(|s, f| s.path() == "V" && f.ident() == "v")
.volatile_static(|s| s.path() == "vol_ptr")
.volatile_static(|s| s.path() == "T1_fn_ptr_vol")
.volatile_fn_arg(|f, p| f.path() == "T1_vol0" && p.ident() == "arg0")
.volatile_fn_arg(|f, p| f.path() == "T1_vol2" && p.ident() == "arg1")
.volatile_fn_return_type(|f| f.path() == "T1_vol1")
.volatile_fn_return_type(|f| f.path() == "T1_vol2")
// The parameter `a` of the functions `T1r`, `T1s`, `T1t`, `T1v` is an array.
.array_arg(|f, p| matches!(f.ident(), "T1r" | "T1s" | "T1t" | "T1v") && p.ident() == "a")
.array_arg(|f, p| matches!(f.path(), "T1r" | "T1s" | "T1t" | "T1v") && p.ident() == "a")
.skip_roundtrip(|n| n == "Arr");
ctest::generate_test(&mut t1gen, "src/t1.rs", "t1gen.rs").unwrap();

Expand Down
25 changes: 20 additions & 5 deletions ctest/src/ast/constant.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
use crate::BoxStr;

/// Represents a constant variable defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Const {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) ty: syn::Type,
}

impl Const {
/// Return the identifier of the constant as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the constant variable as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Const::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}
}
2 changes: 1 addition & 1 deletion ctest/src/ast/field.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::BoxStr;

/// Represents a field in a struct or union defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
Expand Down
25 changes: 20 additions & 5 deletions ctest/src/ast/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ use crate::{
/// Represents a function signature defined in Rust.
///
/// This structure is only used for parsing functions in extern blocks.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Fn {
pub(crate) public: bool,
#[expect(unused)]
pub(crate) abi: Abi,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) link_name: Option<BoxStr>,
#[expect(unused)]
pub(crate) parameters: Vec<Parameter>,
Expand All @@ -21,9 +22,23 @@ pub struct Fn {
}

impl Fn {
/// Return the identifier of the function as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the function item as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Fn::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}

/// Return the name of the function to be linked C side with.
Expand Down
20 changes: 20 additions & 0 deletions ctest/src/ast/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod constant;
mod field;
mod function;
mod module;
mod parameter;
mod static_variable;
mod structure;
Expand All @@ -12,12 +13,31 @@ use std::fmt;
pub use constant::Const;
pub use field::Field;
pub use function::Fn;
pub use module::Module;
pub use parameter::Parameter;
pub use static_variable::Static;
pub use structure::Struct;
pub use type_alias::Type;
pub use union::Union;

use crate::MapInput;

/// Transforms an item's absolute path to use `_` as path separator instead of
/// `::`.
pub(crate) fn escape_item_path<'a>(item: impl Into<MapInput<'a>>) -> String {
let path = match item.into() {
MapInput::Struct(s) => s.path(),
MapInput::Union(u) => u.path(),
MapInput::Fn(f) => f.path(),
MapInput::Alias(a) => a.path(),
MapInput::Const(c) => c.path(),
MapInput::Static(s) => s.path(),

_ => unimplemented!("other MapInput data constructors do not represent rust items"),
};
path.replace("::", "_")
}

/// The ABI as defined by the extern block.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Abi {
Expand Down
63 changes: 63 additions & 0 deletions ctest/src/ast/module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use syn::Path;
use syn::punctuated::Punctuated;

use crate::BoxStr;
use crate::ffi_items::FfiItems;

/// Represents a Rust module. `ctest` only considers items for which there is a
/// corresponding type in this crate.
#[derive(Debug, Clone)]
pub struct Module {
pub(crate) public: bool,
pub(crate) cached_path: BoxStr,
pub(crate) path: Path,
pub(crate) items: FfiItems,
}

impl Module {
/// Returns the full path to the module item.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Module::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}
}

impl Default for Module {
fn default() -> Self {
Self {
public: bool::default(),
cached_path: String::new().into_boxed_str(),
path: Path {
leading_colon: None,
segments: Punctuated::new(),
},
items: FfiItems::default(),
}
}
}

// [NOTE]: a custom implementation is used because FfiItems does not have a
// PartialEq implementation. It is best if it does not have one. The
// current_module field in FfiItems is not meant for use post-parse-time. It
// would likely be unintuitive to have its PartialEq not take into account.
impl PartialEq for Module {
fn eq(&self, other: &Self) -> bool {
self.public == other.public
&& self.cached_path == other.cached_path
&& self.path == other.path
&& self.items.custom_eq(&other.items)
}
}
2 changes: 1 addition & 1 deletion ctest/src/ast/parameter.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::BoxStr;

/// Represents a parameter in a function signature defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
pub(crate) ident: BoxStr,
#[expect(unused)]
Expand Down
25 changes: 20 additions & 5 deletions ctest/src/ast/static_variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,35 @@ use crate::{
///
/// This structure is only used for parsing statics in extern blocks,
/// as a result it does not have a field for storing the expression.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Static {
pub(crate) public: bool,
#[expect(unused)]
pub(crate) abi: Abi,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) link_name: Option<BoxStr>,
pub(crate) ty: syn::Type,
}

impl Static {
/// Return the identifier of the static variable as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the static variable as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Static::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}

/// Return the name of the function to be linked C side with.
Expand Down
25 changes: 20 additions & 5 deletions ctest/src/ast/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@ use crate::{
};

/// Represents a struct defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Struct {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) fields: Vec<Field>,
}

impl Struct {
/// Return the identifier of the struct as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the struct item as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Struct::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}
}
25 changes: 20 additions & 5 deletions ctest/src/ast/type_alias.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
use crate::BoxStr;

/// Represents a type alias defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Type {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) ty: syn::Type,
}

impl Type {
/// Return the identifier of the type alias as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the type alias as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Type::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}
}
25 changes: 20 additions & 5 deletions ctest/src/ast/union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,31 @@ use crate::{
};

/// Represents a union defined in Rust.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Union {
pub(crate) public: bool,
pub(crate) ident: BoxStr,
pub(crate) cached_path: BoxStr,
pub(crate) path: syn::Path,
pub(crate) fields: Vec<Field>,
}

impl Union {
/// Return the identifier of the union as a string.
pub fn ident(&self) -> &str {
&self.ident
/// Return the full path to the union item as a string.
///
/// If inside a nested module, this will return a top-level-relative path,
/// but not a crate-relative path. For some item `foo` in module
/// `crate::bar`, the returned string will be `bar::foo`, and not
/// `crate::bar::foo`.
pub fn path(&self) -> &str {
&self.cached_path
}

/// Returns the last path of the identifier, from the absolute path returned
/// by [`Union::path`].
pub fn ident(&self) -> String {
let Some(syn::PathSegment { ident, .. }) = self.path.segments.last() else {
unreachable!("all parsed items have at least one element in their path")
};
ident.to_string()
}
}
Loading