Boost.System became header-only in Boost 1.69. Boost 1.90.0 no longer ships a compiled library or CMake config for it, so find_package(Boost REQUIRED COMPONENTS system ...) fails.
cmake/CMakeLists.txt:
- set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
+ set(BOOST_COMPONENTS program_options regex thread date_time iostreams)The headers are still available; no linking is needed.
Calling a template member function through a dependent expression (e.g. storage->has_attribute_value<T>(...) where storage's type depends on a template parameter) requires the template keyword to disambiguate from a less-than comparison.
src/ifcparse/IfcParse.cpp:1856:67: error: expected primary-expression before '>' token
1856 | if (storage->has_attribute_value<express::Base>(attr_index)) {
| ^
Six identical errors at lines 1856, 1865, 1896, 1905, 1934, 1943.
src/ifcparse/IfcParse.cpp:
-storage->has_attribute_value<express::Base>(attr_index)
+storage->template has_attribute_value<express::Base>(attr_index)
-storage->has_attribute_value<Blank>(attr_index)
+storage->template has_attribute_value<Blank>(attr_index)Applied at all six call sites in in_memory_file_storage::read_from_stream.
InstanceStreamer is a class template with methods defined in IfcParse.cpp, not the header. Without explicit instantiations, the linker can't find the symbols when the SWIG wrapper loads.
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPS3_PNS_7IfcFileE
(IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcFile*))
Cannot use template class InstanceStreamer<...> because some constructors have static_assert guards that reject certain reader types. Instead, instantiate each member function individually per reader type, only including the constructors valid for that type.
src/ifcparse/IfcParse.cpp (after the last InstanceStreamer method definition):
// FullBufferImpl
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(const std::string&, bool, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcParse::IfcFile*);
// ... plus ensure_header, initialize_header, hasSemicolon, semicolonCount,
// pushPage, bypassTypes, readInstance
// PushedSequentialImpl — same pattern, different valid constructors
// MMapFileReader (ifdef USE_MMAP) — same patternSWIG's stream_from_string calls InstanceStreamer<FileReader<FullBufferImpl>>(void*, int, IfcFile*), but the (void*, int) constructor previously hit a static_assert for FullBufferImpl — it only allowed PushedSequentialImpl.
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPviPNS_7IfcFileE
(InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcFile*))
Three changes to make FullBufferImpl support buffer-based and default construction:
src/ifcparse/FileReader.h — add buffer constructor to FullBufferImpl:
class IFC_PARSE_API FullBufferImpl {
public:
explicit FullBufferImpl(const std::string& fn);
+ FullBufferImpl(void* data, size_t length);src/ifcparse/FileReader.h — add FileReader(void*, size_t) forwarding constructor:
+ FileReader(void* data, size_t length)
+ : cursor_(0) {
+ if constexpr (std::is_same_v<Impl, FullBufferImpl>) {
+ impl_ = std::make_shared<Impl>(data, length);
+ } else {
+ static_assert(...);
+ }
+ }src/ifcparse/FileReader.cpp — implement the constructor:
FullBufferImpl::FullBufferImpl(void* data, size_t length)
: buf_(static_cast<char*>(data), static_cast<char*>(data) + length)
, size_(length) {
}src/ifcparse/IfcParse.cpp — extend the two InstanceStreamer constructors to accept FullBufferImpl:
// InstanceStreamer(IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(nullptr, (size_t)0);
// InstanceStreamer(void*, int, IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(data, (size_t)length);parse_context_pool stores nodes in a std::vector<parse_context>. During parsing, load() takes a parse_context& parameter and calls context.push(), which calls pool_->make(). If the pool's vector reallocates (via emplace_back), all existing references into the vector — including the context reference held by the caller — become dangling. Subsequent access through the dangling reference causes a segfault.
Triggered by larger IFC files (e.g. ISSUE_159_kleine_Wohnung_R22.ifc, 9.5 MB) that cause enough pool growth to trigger reallocation.
Thread 1 received signal SIGSEGV, Segmentation fault.
0x... in IfcParse::parse_context::push()
#1 in_memory_file_storage::load(...) // context& is dangling after reallocation
#2 in_memory_file_storage::load(...) // parent call
#3 InstanceStreamer::readInstance()
src/ifcparse/storage.h — change the pool container from std::vector to std::deque, which does not invalidate references on push_back/emplace_back:
+#include <deque>
struct parse_context_pool {
- std::vector<parse_context> nodes_;
+ std::deque<parse_context> nodes_;express::Base::operator< and operator== called data(), which throws std::runtime_error("Trying to access deleted instance reference") when the internal weak_ptr is expired. A default-constructed express::Base (the value-type equivalent of a null pointer) always has an expired weak_ptr.
The bug requires two conditions to coincide:
- A representation is shared by more than one product (via
IfcRepresentationMap/IfcMappedItem). - At least one of those products has no material association, so
get_single_material_association()returnsexpress::Base{}(the null equivalent).
In advanced_model.ifc, Body representations like #449 (Body/Brep) have a single IfcRepresentationMap (#453) with 13 IfcMappedItem usages, meaning 13 products share the geometry. Some of those products (e.g. IfcFlowTerminal instances) have no IfcRelAssociatesMaterial, so get_single_material_association returns express::Base{}.
Smaller or simpler models don't hit this because either:
- Every representation maps to only 1 product →
reuse_ok_short-circuits atproducts.size() == 1before reaching the material check. - Every product has a material association → no null
express::Baseis ever inserted into the set.
Iterator::initialize()
try {
mapping::get_representations(reps, filters_)
addRepresentationsFromDefaultContexts(representations)
→ collects reps from subcontexts in order:
Axis (#115): 143 reps
Body (#117): 7550 reps
FootPrint (#119): 12 reps
for (auto representation : representations):
── Axis reps (indices 0–142) ──────────────────────────
products_represented_by(rep, rmap)
→ OfProductRepresentation: 1 product each
filter_products(products, filters) → 1 product
reuse_ok_(ifcproducts)
→ products.size() == 1 → return true ← SHORT-CIRCUIT, no material check
representation_mapped_to(rep) → null (no MappedItem)
→ task created. 143 tasks accumulated.
── First Body rep #449 (Body/Brep) ────────────────────
products_represented_by(#449, rmap)
→ OfProductRepresentation: empty
→ RepresentationMap: 1 map (#453)
→ MapUsage: 13 MappedItems → traces through to 13 IfcProducts
filter_products(products, filters) → 13 products
reuse_ok_(ifcproducts) ← CRASH HERE
→ products.size() == 1? NO (13 products)
→ for each product:
find_openings(product) → OK
get_single_material_association(product)
→ some products have no IfcRelAssociatesMaterial
→ returns express::Base{} (expired weak_ptr)
associated_single_materials.insert(result)
→ std::set::insert calls operator<
→ operator< calls data()
→ data() calls data_.lock() → expired → THROWS
"Trying to access deleted instance reference"
} catch (const std::exception& e) {
Logger::Error(e) ← exception caught here, get_representations aborted
}
→ reps contains only the 143 Axis tasks created before the throw
→ all 143 Axis reps have Curve2D geometry → map(representation) returns null
→ no valid elements produced → initialize() returns false
In the old pointer-based code, reuse_ok_ used std::set<const IfcUtil::IfcBaseEntity*> and get_single_material_association returned nullptr. Inserting nullptr into a std::set<T*> is a plain pointer comparison — no dereference, no throw. The refactoring to std::set<express::Base> changed the comparison from pointer comparison to express::Base::operator<, which unconditionally dereferences through data().
[Error] Trying to access deleted instance reference
[Notice] Created 143 tasks for 143 products ← only Axis reps; all Body reps lost
initialize() returned: False
src/ifcparse/express.h — use weak_ptr::lock().get() instead of data() so that expired pointers compare as nullptr (matching old raw-pointer semantics):
bool operator<(const Base& other) const {
- return data() < other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() < b.get();
}
bool operator==(const Base& other) const {
- return data() == other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() == b.get();
}Accessing inverse attributes (e.g. element.IsDecomposedBy) on any entity raises AttributeError: entity instance of type 'IFC2X3.IfcProject' has no attribute 'get_inverse'.
entity_instance_mixin.__getattr__ (line 106 of entity_instance.py) calls self.get_inverse(name) when it detects an inverse attribute. Since the mixin inherits into the SWIG-generated entity_instance class (via the object = custom_base hack in IfcParseWrapper.i:936), self.get_inverse must resolve to a method on the SWIG class.
However, IfcParseWrapper.i:70 has a global rename:
%rename("get_inverses_by_declaration") get_inverse;
This was intended for ifcopenshell::file::get_inverse (which takes an entity + declaration and returns instances by reference), but SWIG %rename is global — it also renames the %extend express::Base method get_inverse(const std::string& a) at line 551. So the Python-side entity_instance class exposes the method as get_inverses_by_declaration, not get_inverse.
The old code (v0.8.0) didn't hit this because __getattr__ called self.wrapped_data.get_inverse(name) on an inner ifcopenshell_wrapper.entity_instance object — but in that old layout, the inner object was constructed differently and the rename didn't apply the same way (or the method had a different path). In the new mixin approach, self is the SWIG object, so the rename is directly visible.
src/ifcwrap/IfcParseWrapper.i — override the global rename specifically for express::Base::get_inverse, restoring the original name on entity instances:
+%rename("get_inverse") express::Base::get_inverse;
%rename("get_inverses_by_declaration") get_inverse;Add this line before the global rename (or anywhere before the %extend express::Base block). This scoped rename takes precedence for express::Base, so:
entity_instance.get_inverse(name)works as the mixin expectsfile.get_inverses_by_declaration(...)keeps its intended name
entity_instance.py:106 — call the method by its SWIG-renamed name:
- vs = self.get_inverse(name)
+ vs = self.get_inverses_by_declaration(name)The class rename from entity_instance to entity_instance_mixin broke external code that does from ifcopenshell.entity_instance import entity_instance.
ImportError: cannot import name 'entity_instance' from 'ifcopenshell.entity_instance'
Triggered at import time via ifcopenshell.util.pset (and likely other modules).
src/ifcopenshell-python/ifcopenshell/entity_instance.py — add a backwards-compatible alias at the bottom of the module:
entity_instance = entity_instance_mixin