Skip to content

Rust API ​

Browse the generated API reference →

Every public item across the 33 workspace crates is documented, and missing_docs is denied workspace-wide, so an undocumented public item is a build failure rather than a review comment.

The reference is rebuilt from source on every push to main and published with this site. The verification gate builds it with RUSTDOCFLAGS="-D warnings", so a broken intra-doc link fails CI. Released crates are also on docs.rs; each crate's reference page links both. To read the reference locally:

bash
cargo doc --workspace --all-features --no-deps --open

Orientation ​

The generated reference is exhaustive; this page is the map.

ifc-model — start here ​

TypeRole
ModelThe entity graph. Insert, get, iterate, index by type
EntityType name plus positional attributes
ValueThe encoding-independent value model
EntityIdStable in-file identity (#42)
HeaderFile metadata and the declared schema token
CodecThe read/write contract every encoding implements
GlobalIdIFC's base-64 GUID encoding

Value is the type to understand first, because everything in the graph is one:

VariantSTEP formNote
Null$Attribute not set
Derived*Derived in a supertype — distinct from Null
Bool.T. / .F.
LogicalUnknown.U.The third boolean state
Integer, Real42, 2.5
Text'wall'Already unescaped to UTF-8
Binary"0123ABC"
Enum.ELEMENT.Unquoted constant
Ref#42Reference to another entity
List(...)List, set, array, or bag
TypedIFCLENGTHMEASURE(2.5)Typed wrapper

Null and Derived are not the same

Collapsing * to $ corrupts files on write. IFC uses * to mean "this attribute is redeclared as derived in a subtype", which is semantically distinct from "not set".

Reading attributes ​

Entity offers positional accessors with typed convenience:

rust
let first: Option<&Value> = entity.attribute(0);
let name: Option<&str> = entity.text(2);
let height: Option<f64> = entity.number(3);
let placement: Option<EntityId> = entity.reference(4);
let outgoing: Vec<EntityId> = entity.references(); // every outgoing reference
let is_wall: bool = entity.is_type("IFCWALL");

Index constants belong in named *_slot modules, following the pattern in ifc-geometry — bare numeric literals at call sites are how attribute bugs get written.

Querying the model ​

rust
let walls: &[EntityId] = model.ids_of_type("IFCWALL"); // indexed, not a scan
let pairs: Vec<(EntityId, &Entity)> = model.of_type("IFCWALL").collect();
let histogram: Vec<(&str, usize)> = model.type_histogram(); // good for triage
let dangling: Vec<(EntityId, EntityId)> = model.dangling_references(); // (from, missing)

Build the optional reverse index only when an operation needs incoming references:

rust
use ifc_model::{EntityId, Model, ReverseIndex};

fn print_referrers(model: &Model, target: EntityId) {
    let reverse = ReverseIndex::build(model);
    for hit in reverse.referrers(target) {
        println!(
            "referenced by {:?} in attribute slot {}",
            hit.from, hit.slot
        );
    }
}

The index is a deterministic snapshot and records the top-level attribute slot for every referrer. Rebuild it after mutating the model.

Reverse indexes are deliberately on demand

Codecs that only read and rewrite a model do not pay the memory or load-time cost. Traversal-heavy applications build the index once and reuse it.

Codecs ​

rust
use ifc::{Codec, StepCodec};

let model = StepCodec.read_bytes(bytes)?;
let out = StepCodec.write_bytes(&model)?;

XmlCodec behaves identically behind the ifcxml feature. Conversion is a read with one and a write with the other.

Units ​

ifc::properties converts between units: SI units with any prefix (by an exact power of ten), the named conversion-based units of IFC4 Table 673 with their exact factors, and a model's own unit entities (resolve_unit), refusing units of different dimensions, monetary units and offset units. ifc::convert_length_unit gives a whole model a new length unit, rescaling every length, area and volume the release's measure types identify and refusing by name what it cannot carry.

rust
use ifc::properties::{convert_value, ConversionUnit};
use ifc::{convert_length_unit, LengthUnitOptions};

let foot = ConversionUnit::parse("foot")?;
let millimetre = ConversionUnit::parse("MILLIMETRE")?;
assert_eq!(convert_value(1.0, &foot, &millimetre)?, 304.8);

// Every length, area and volume, in one transaction, or nothing.
let metre = ConversionUnit::parse("METRE")?;
let report = convert_length_unit(&mut model, &metre, &LengthUnitOptions::new())?;
println!(
    "{} values rescaled by {}",
    report.values_converted,
    report.length.factor()
);

Reading a file without decoding it ​

A strict read is already lazy (ADR 0015): StepCodec.read_bytes validates every record but decodes an entity only when it is first accessed, and the model keeps the file's bytes. Type queries, ids and counts need no decoding; Model::decode_all(threads) decodes the rest in parallel when a consumer is about to touch everything, and StepReader::eager() restores the old decode-everything read.

109 MB Revit IFC, 1.3 M recordstimepeak resident
eager read1.38 s616 MB
lazy read0.39 s341 MB
lazy read + decode_all(8)0.71 s635 MB

Index::scan goes one step further for a census or a subset: it borrows the bytes, frames every record with openbim_step::scan and keeps its id, span and type, without validating the inside of a record. Framing is strict -- junk between records or a second appended file is an error. Index::entity decodes one record, materialize_closure builds a real Model (with the file's header) from a subset plus everything it references, and materialize gives the raw subset. Decode failures are returned as errors.

Scale and memory ​

A strict read validates every record up front and decodes an entity only when it is first touched (see above and ADR 0015). Memory is therefore the file's bytes plus whatever has been decoded. A consumer that touches every entity pays for a fully decoded model, which is several times the file size.

crates/ifc-step/tests/scale.rs holds a full parse of a large synthetic model under 10x the input size in resident memory, so a regression fails the gate. benchmarks/README.md records comparative measurements against IfcOpenShell, and the committed read, write, lookup and memory baseline, each with the machine and method it was taken on.

Other languages ​

The same core is published for JavaScript and TypeScript (npm), Python (PyPI) and C. See Install and the bindings.

Released under the AGPL-3.0-or-later licence. ISO and CEN standards material is not redistributed.