read-only
Read-only field exposure and safe composition helpers via proc macros.
This crate intentionally combines two patterns:
#[cast]is the classic cast-based readonly-field pattern, re-exported fromreadonly::make.#[embed]is this crate's safe composition-based alternative that builds a dedicatedReadOnly*view struct and dereferences into it without unsafe code.
Installation
Add this to your Cargo.toml:
[]
= "0.1"
Usage
Two attribute macros are provided.
#[cast]
[!NOTE]
#[cast]is a re-export ofreadonly::make. You can use eitherread_only::castorreadonly::make— they are the same proc macro.
Implements Deref via an unsafe pointer cast — wrapped safely so callers
never write unsafe themselves. Fields remain writable inside the defining
module but become read-only externally.
All fields read-only (no #[readonly] annotation needed):
use cast;
// Inside this module: config.timeout = 5; // ✅ write
// Outside: config.timeout = 5; // ❌ compile error
// let t = config.timeout; // ✅ read
Selective read-only (annotate specific fields with #[readonly]):
use cast;
// Inside this module: dev.mutable_count = 1; // ✅ write
// dev.serial = 2; // ✅ write (inside module)
// Outside: dev.mutable_count = 1; // ✅ write (not readonly)
// let s = dev.serial; // ✅ read only
// dev.serial = 2; // ❌ compile error
When no #[readonly] attribute appears on any field, the entire struct
becomes read-only through a generated view struct. When any field is annotated,
only annotated fields are lifted into the read-only view; unannotated fields
keep their original mutability.
#[embed]
Moves #[readonly] annotated fields into a ReadOnly* struct that is embedded
inside the original struct. A safe Deref implementation is generated — no
unsafe code is involved.
use embed;
// let mut dev = Device::new(42, "dev1", 0);
// let h = dev.handle; // ✅ read — goes through Deref to ReadOnlyDevice
// let l = &dev.label; // ✅ read — goes through Deref
// dev.mutable = 1; // ✅ write — mutable stays on the outer struct
// dev.handle = 0; // ❌ compile error — handle is read-only
[!NOTE] The generated
ReadOnly*struct is public. You can implementDeref,Display, or other traits on it to forward or customize behavior.
Choosing a Macro
- Use
#[cast]if you want the familiarreadonlybehavior and the smallest syntax change. - Use
#[embed]if you want an explicit readonly view type and a safe composition-based implementation with no unsafe code in the generated access path.
Examples
Runnable examples are available under examples/:
cargo run --example basiccargo run --example embed_deref
Alternatives
Directly comparable:
readonly: the canonical cast-based readonly-field crate and the upstream implementation re-exported here as#[cast].
Related accessor-generation crates:
getset: generates getter/setter methods instead of preserving direct field reads.derive-getters: generates public getter methods for named structs.fieldwork: a broader accessor macro system for structs and enums with many generated method styles.
The main difference is that those crates expose access through generated
methods, while read-only focuses on preserving direct readonly field access
syntax.
Testing
The workspace includes both compile-time macro tests and runtime tests.
trybuildmacro tests cover pass/fail macro expansion behavior.tests/miri.rsexercises the#[cast]unsafe pointer-cast path under Miri to help detect undefined behavior and provenance issues.
Run the Miri suite with:
For source coverage, the CI allows one uncovered line/region for the remaining LLVM coverage artifact in the proc-macro expansion code:
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.