Commit Graph

760 Commits

Author SHA1 Message Date
Dion Moult 37aa68e66c ifcopenshell plug-in loader: stable anchor + bundle-aware fallbacks
# What

Two upstream-shaped fixes to ifcopenshell's runtime plug-in discovery
so the schema/serializer plug-ins are findable in deployment layouts
other than a flat \`<prefix>/lib/\` (specifically: macOS .app bundles).

## (1) Stable anchor variable instead of a function pointer

\`schema_plugin_directory()\` used \`&load_schema_plugins\` as the
anchor whose containing module \`dladdr\` is asked to resolve. Function
addresses are not reliably equal to a single canonical location across
toolchains — on macOS arm64 with BonsaiViewer.app, \`&load_schema_plugins\`
took the address of a PLT/stub inside the consumer binary rather than
the actual symbol inside \`libIfcParse.dylib\`. \`dladdr\` then dutifully
returned the consumer's path and the loader started searching
\`BonsaiViewer.app/Contents/MacOS/\` for plug-ins that were never
installed there.

A variable doesn't suffer from this — it has exactly one canonical
address inside its defining dylib. Add \`ifcopenshell_libifcparse_anchor\`
(exported via IFC_PARSE_API) and use \`&that\` instead. Standard pattern
used by Boost.DLL, GStreamer, \`_dyld_get_image_*\`, etc.

## (2) Bundle-aware fallback search paths

The primary search path is \`dirname(libIfcParse)\`. That works for
flat installs (Linux \`lib/\`, Windows \`bin/\`) where plug-ins are
siblings of libIfcParse. macOS app bundles split the layout:
\`macdeployqt\` puts non-Qt @rpath deps in \`Contents/Frameworks/\`,
Apple convention asks for \`Contents/PlugIns/\`, and some install rules
co-locate libIfcParse with the exe in \`Contents/MacOS/\`. Plug-ins
typically end up in a sibling directory, not the same one.

In \`add_search_paths_or_default\`, after registering the primary path,
also register \`<parent>/PlugIns\`, \`<parent>/Frameworks\`, and
\`<parent>/MacOS\` on Apple platforms. \`discover_exact\` short-circuits
on the first hit so duplicates and missing directories are harmless.

# What this does NOT do

The plug-in dylibs still need to actually be inside the app bundle
somewhere for these fallbacks to find them — the upstream install
rules (\`install(TARGETS …)\` in \`src/ifcparse/CMakeLists.txt\`,
\`src/serializers/CMakeLists.txt\`, etc.) put them in \`<prefix>/lib/\`
which lives outside \`BonsaiViewer.app\`. That side of the fix is a
follow-up — either an explicit bundle-aware install destination on
the plug-in targets, or an install(CODE) sweep that mirrors them
into the bundle.

# What this also un-does

Reverts the BonsaiViewer-only \`install(CODE)\` hack that was about to
copy \`ifcopenshell.*.dylib\` from \`lib/\` into
\`BonsaiViewer.app/Contents/MacOS/\` — superseded by the loader-side
fix above, which lets us put the plug-ins anywhere sane inside the
bundle without further consumer-side stitching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:18:59 +10:00
Dion Moult 19bff92a47 Fix data races when parsing IFC files on concurrent threads
Loading a federated project (.ifcfed) with several models segfaults
non-deterministically on a fresh start. The viewer's SceneLoader spawns
one background std::thread per model in startDataSourceLoad() to
construct an ifcopenshell::file; with cached sidecars all models reach
that point near-simultaneously, so multiple threads parse different IFC
files at once. Parsing touches the process-wide schema singleton, which
was not thread-safe in two places.

Race 1 — concurrent schema population
-------------------------------------
schema_registry::get() lazily runs the schema's get_() function (e.g.
Ifc4::get_schema() -> IFC4_populate_schema()) and mutates entries_ with
no lock. Two threads calling schema_by_name("IFC4") at once both run
IFC4_populate_schema() concurrently, which fills global arrays
(IFC4_types[], strings[]). One thread reads a slot the other is still
writing.

Core-dump evidence (gdb thread apply all bt):

  Thread 1  SIGSEGV in IFC4_populate_schema   Ifc4-schema.cpp:1989
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- schema_by_name("IFC4")
            <- ifcopenshell::file::file (NWCH-PIR-SS...ifc)
            <- SceneLoader::startDataSourceLoad lambda  SceneLoader.cpp:315

  Thread 3  also in IFC4_populate_schema (entity ctor for
            "IfcMaterialProfileSetUsageTapering")
            <- Ifc4::get_schema
            <- schema_registry::get           schema.cpp:241
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

Two threads inside IFC4_populate_schema() at the same time is the race.

Fix: guard schema_registry's bind()/get()/names()/clear() with a
recursive_mutex (recursive because get() re-enters bind() via
load_schema_plugin(), and a freshly populated schema registers itself
through register_schema()). get() is serialized, so only the first
thread populates the schema; the rest block briefly and then observe
the finished result. Returned schema pointers are stable for the
process lifetime, so holding the lock only across get() is sufficient.

Race 2 — lazy all_attributes_ cache filled during parsing
---------------------------------------------------------
entity::all_attributes() lazily fills a `mutable` optional cache on the
shared schema entity the first time it is accessed — and that first
access happens during parsing (parse_context::construct), not during
schema population. With race 1 fixed, two parser threads still raced
here: both saw the cache empty, both did all_attributes_.emplace() and
std::copy() into it, corrupting the vector.

Core-dump evidence after the race-1 fix:

  Thread 1  SIGSEGV in attribute::type_of_attribute (this=0xe130...55c)
            <- std::transform(first=0x4, last=0xb0d1...)   <-- garbage
               iterators into a corrupt std::vector
            <- parse_context::construct over
               decl->as_entity()->all_attributes()        file.cpp:249
            <- instance_streamer::read_instance
            <- ifcopenshell::file::file (NWCH-PIR-PT...ifc)
            <- SceneLoader::startDataSourceLoad lambda

The begin pointer 0x4 is a half-written vector being read mid-resize by
another thread.

Fix: force every entity's all_attributes_ cache in the
schema_definition constructor, while construction is still
single-threaded. The schema is then genuinely immutable after
construction, so concurrent parsing needs no hot-path lock.

Both crashes reproduce reliably on a fresh start at native speed but
vanish under gdb (which serializes thread scheduling) — the classic
signature of a data race. With both fixes the federated load completes
cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:34:19 +10:00
Thomas Krijnen a1efdccb4b Add back mutex 2026-05-08 10:07:34 +02:00
Thomas Krijnen b1899b1a8d Expand schema_plugin with schema name to eliminate symbol collisions 2026-05-07 21:10:35 +02:00
Thomas Krijnen 39d583a26a Merge remote-tracking branch 'origin/ifcviewer' into datamodel-v1.0 2026-05-07 21:01:56 +02:00
Thomas Krijnen e893552f24 Fixes to plug-in loading in and outside of pyodide 2026-05-07 14:43:26 +02:00
Dion Moult 0bb6df6a6b ifcparse: skip flush+compact for read-only RocksDB on destruction
Read-only handles reject Flush/CompactRange, so the destructor's status
assertion always fired on shutdown when the streamer's sidecar was
opened with read_only=true. Track the flag and skip the write path; also
guard against a null db when the initial open failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:16:15 +10:00
Thomas Krijnen 2257d7930a For schema plugins also wasm-opt -O1 2026-05-06 21:38:08 +02:00
Thomas Krijnen 98ff457fd6 Continue work on plug-in and tests 2026-05-06 21:17:57 +02:00
Thomas Krijnen 4670715ef3 Work a bit on failing tests 2026-05-06 11:41:43 +02:00
Thomas Krijnen ddfe3bce20 Fixes for WASM build (some temporary) 2026-04-24 13:36:06 +02:00
Dion Moult 543d9f8588 Local hacks to compile and monkey patch issues in the Python world
All AI generated slop. Do NOT trust these "fixes". It's just to get it
working on my machine.
2026-04-23 21:32:24 +10:00
Thomas Krijnen c42a7f32d0 The proper id / identity fix for rocksdb 2026-04-22 18:11:01 +02:00
Thomas Krijnen 14e9846e35 identity_ for types; id_ for instances 2026-04-22 12:04:24 +02:00
Thomas Krijnen 37c6aea092 forgot to set goosd 2026-04-22 12:04:09 +02:00
Thomas Krijnen 9b13dc8dd6 Get rid of parse context pool 2026-04-22 12:03:58 +02:00
Thomas Krijnen 13c71d7a8a symbol visibility 2026-04-21 21:48:54 +02:00
Thomas Krijnen b022ca7e70 Some plug-in work 2026-04-21 16:18:59 +02:00
Thomas Krijnen 325db2e57f Export templates 2026-04-21 11:55:04 +02:00
Thomas Krijnen 046ceb452a Tighten scope of cmake vars and dirs 2026-04-19 12:32:18 +02:00
Thomas Krijnen 6c47123781 Remove C++ references to ifcxml 2026-04-19 10:35:04 +02:00
Thomas Krijnen a25f522cc3 typo 2026-04-19 08:33:48 +02:00
Thomas Krijnen bccea6d932 Examples and update virtual bases for new codegen 2026-04-18 21:04:42 +02:00
Thomas Krijnen 1cc93784cd Rerun codegen 2026-04-18 21:03:02 +02:00
Thomas Krijnen 91ae631c7d Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0 2026-04-18 20:15:28 +02:00
Thomas Krijnen d2cc66fdf0 tree and document plug-ins 2026-04-17 11:24:09 +02:00
Thomas Krijnen 3824e7b449 First start plug-in architecture 2026-04-15 18:07:28 +02:00
Thomas Krijnen aa10784154 First slice plug-in refactor 2026-04-14 13:51:09 +02:00
Thomas Krijnen 11006f0ef5 deque 2026-04-13 21:18:58 +02:00
Thomas Krijnen e7db239647 inverse access in schema 2026-04-10 21:46:39 +02:00
Thomas Krijnen b2fc0c00cc Hierarchical index for inverses 2026-04-10 14:54:47 +02:00
Thomas Krijnen becd38c77d Compilation fixes 2026-04-09 16:18:53 +02:00
Thomas Krijnen 3fbf01f446 partial revert of 24acfea 2026-04-08 13:48:23 +02:00
Thomas Krijnen 24acfeaf45 Thinking indicator under chat 2026-04-03 10:59:09 +02:00
Thomas Krijnen 20ccf2b455 Parameter naming 2026-03-31 18:23:13 +02:00
Thomas Krijnen a07f56db6f Restructure and rename 2026-03-31 15:32:36 +02:00
Thomas Krijnen 724cdb446e Move schemas into schemas/ subfolder 2026-03-31 10:02:00 +02:00
Thomas Krijnen 95b3f7dac4 Don't include dot as special when doing float runs 2026-03-27 21:04:48 +01:00
Thomas Krijnen 603cedc487 Try some things: (a) fewer allocations - parse context pool; lexer string pool (b) SWAR process multiple chars at once in keywords/enums/strs/stc. 2026-03-27 20:45:13 +01:00
Thomas Krijnen 8e42f35db3 Rework variable length token storage to use string pool; eliminate need for rereads 2026-03-26 15:49:28 +01:00
Thomas Krijnen 9d69a712ac Does SWIG prefer std::conditional_t over auto return type? 2026-03-26 11:33:00 +01:00
Thomas Krijnen dc127471c5 Rerun codegen 2026-03-26 10:52:12 +01:00
Thomas Krijnen 95a094d596 Fix some schema generation issues 2026-03-26 10:39:29 +01:00
ssg3d 7b4889d2ec Update IfcParse.cpp
IfcOpenshell read file, and write file without changes. This round trip introduces truncation noise. It should not hurt to increase the precision to keep this clean.
2026-02-13 10:03:05 +01:00
Thomas Krijnen 4ec643d4e1 schema entity initialize() method, optional argument forwarding in file::create, populate_derived in InstanceData 2026-01-15 19:24:55 +01:00
Thomas Krijnen d82e1da907 Don't leak parent id into type decl instances 2026-01-15 15:38:49 +01:00
Thomas Krijnen 574827016a Initialization of header and file 2026-01-14 14:03:28 +01:00
Thomas Krijnen 1a4d750ecd Consistency of get_inverse calls 2026-01-14 14:00:50 +01:00
Thomas Krijnen 0a5dd78774 Fix add entity with id 2026-01-14 14:00:00 +01:00
Thomas Krijnen e4e4f31d20 Defer deletion so that traversal still works 2026-01-13 08:45:04 +01:00