Architecture
ASC FDL is a multi-language toolkit built around a single shared C library. This document explains how the layers fit together and why the project is structured this way.
Layer Diagram
+-----------------------------------------------------+
| Application Code |
| (Python scripts, C++ programs, etc.) |
+----------------------+------------------------------+
| Python Facade | C++ RAII Header |
| fdl/ | fdl/fdl.hpp |
| (idiomatic classes)| (RAII wrapper classes) |
+----------------------+ |
| Python FFI | |
| fdl_ffi/ | |
| (ctypes bindings) | |
+----------------------+------------------------------+
| C ABI |
| fdl_core.h (fdl_doc_*, fdl_canvas_*, ...) |
+-----------------------------------------------------+
| libfdl_core (C++ shared library) |
| native/core/src/ -- JSON parsing, validation, |
| geometry, templates, builder, custom attrs |
+-----------------------------------------------------+
Design Decisions
Why a C ABI? A stable C ABI is the most portable FFI boundary. Any
language with a foreign-function interface (Python ctypes, Node.js ffi-napi,
Rust extern "C", Swift, etc.) can call C functions directly. The C++ core
compiles to a shared library (libfdl_core.dylib/.so/.dll) exporting
only C-linkage symbols, so binding authors never depend on C++ name mangling,
exceptions, or STL types.
Why opaque handles? All domain objects (documents, contexts, canvases,
framing decisions, etc.) are represented as opaque pointers (fdl_doc_t*,
fdl_canvas_t*, ...). This allows the internal C++ implementation to change
freely without breaking the ABI. Callers interact through accessor functions
rather than struct field offsets.
Why two Python layers? The Python binding is split into a low-level FFI
layer (fdl_ffi/) and a high-level facade layer (fdl/). Both are
auto-generated from fdl_api.yaml via code generation. The FFI layer is a
mechanical mapping of C function signatures to ctypes declarations -- it
contains no logic. The facade layer provides idiomatic Python classes with
properties, context managers, and Pythonic iteration. Separating the layers
means the FFI can be regenerated without touching hand-written base classes,
and the facade classes can evolve their API surface independently.
Data Flow
FDL JSON file
|
v
fdl_doc_parse_json() <- C ABI entry point
|
v
Opaque fdl_doc_t* <- Internal: C++ FdlDocument with JSON tree
|
+--> fdl_doc_contexts_count / fdl_doc_context_at <- collection traversal
| |
| v
| fdl_context_t* -> fdl_canvas_t* -> fdl_framing_decision_t*
|
v
Python: FDL (OwnedHandle) -> Context -> Canvas -> FramingDecision
C++: fdl::FDL -> fdl::Context -> ...
Parsing produces an opaque document handle. Sub-objects (contexts, canvases,
framing decisions, framing intents, canvas templates) are accessed via
index-based collection traversal: count() + at(index) + find_by_id().
The Python facade wraps these in CollectionWrapper for Pythonic len(),
[], and for iteration.
Ownership Model
The C core uses a document-owns-everything model:
fdl_doc_t*is the only heap-allocated handle. The caller owns it and must callfdl_doc_free()when done.- All other handles (
fdl_context_t*,fdl_canvas_t*, etc.) are borrowed pointers into the document's internal data. They are valid until the document is freed. - Strings returned as
const char*are borrowed (thread-local, valid until the next call for the same field on the same thread). Strings returned aschar*are caller-owned and must be freed withfdl_free().
The Python facade mirrors this with two base classes (defined in
native/bindings/python/fdl/base.py):
OwnedHandle-- wrapsfdl_doc_t*. Callsfdl_doc_free()on close/exit. Supportswithstatements and warns if not closed explicitly. Stores athreading.Lockfor safe close operations.HandleWrapper-- wraps borrowed sub-object handles. Stores a_doc_refback-reference to the owningOwnedHandle, preventing the Python garbage collector from freeing the document while child handles are still alive.
Thread Safety
The C core provides per-document mutex locking:
- Different documents on different threads: safe
- Same document, concurrent reads: safe (serialized by mutex)
- Same document, concurrent reads + writes: safe (serialized)
fdl_doc_free()during operations: NOT safe (caller must synchronize)
String accessors return thread-local pointers keyed by field name, valid until the next call for the same field on the same thread.
The Python OwnedHandle.close() is protected by a threading.Lock.
Core Module Map
The C++ implementation in native/core/src/ is organized by domain:
| Module | Files | Responsibility |
|---|---|---|
| Document | fdl_doc.cpp, fdl_doc_api.cpp |
JSON parsing, serialization, document lifecycle |
| Validation | fdl_validate.cpp, fdl_validate_api.cpp |
JSON Schema (Draft 2020-12) + semantic validation |
| Geometry | fdl_geometry.cpp, fdl_geometry_api.cpp |
Dimension normalization, scaling, hierarchy gap-filling, cropping |
| Pipeline | fdl_pipeline.cpp, fdl_pipeline_api.cpp |
Scale factor calculation, output sizing, alignment shifts |
| Template | fdl_template.cpp, fdl_template_api.cpp |
Full canvas template application pipeline |
| Builder | fdl_builder.cpp, fdl_builder_api.cpp |
Programmatic document construction (create, add, set) |
| Framing | fdl_framing.cpp, fdl_framing_api.cpp |
Framing-from-intent computation, anchor adjustment |
| Custom Attrs | fdl_custom_attr.cpp, fdl_custom_attr_api.cpp |
Per-object key/value metadata (8 types, 19 functions each) |
| Rounding | fdl_rounding.cpp |
Configurable rounding (even/whole, up/down/round) |
| Handles | fdl_handles.cpp |
Index-based handle resolution and deduplication |
| Canonical | fdl_canonical.cpp |
Key ordering per FDL specification |
| Value Types | fdl_value_types.cpp |
Dimension/point arithmetic and comparison |
| ABI | fdl_abi.cpp |
ABI version reporting |
Each module follows a pattern: internal implementation (fdl_foo.cpp with
fdl_foo.h) and a thin _api.cpp file that exposes functions through the
public C ABI header (fdl_core.h).
Directory Structure
native/
+-- api/fdl_api.yaml # IDL -- single source of truth
+-- core/ # C++ shared library (CMake)
| +-- include/fdl/fdl_core.h # public C ABI header
| +-- src/ # implementation modules
| +-- tests/ # Catch2 C++ unit tests
| +-- Doxyfile # Doxygen configuration
| +-- CMakeLists.txt # build configuration
+-- bindings/
| +-- python/
| | +-- fdl_ffi/ # auto-generated ctypes (low-level)
| | +-- fdl/ # auto-generated facade + hand-written base
| | +-- tests/ # pytest suite
| +-- cpp/
| +-- fdl/fdl.hpp # auto-generated RAII header
+-- tools/
+-- codegen/ # code generation pipeline
+-- fdl_idl.py # IDL parser
+-- ir.py # intermediate representation
+-- adapters.py # language-specific type adapters
+-- generate.py # entry point
+-- templates/ # Jinja2 templates (python/, cpp/)
packages/
+-- fdl_imaging/ # OpenImageIO-based image processing
+-- fdl_frameline_generator/ # frameline overlay generation
+-- fdl_viewer/ # PySide6 desktop application
schema/ # JSON Schema definitions (v0.1-v2.0.1)
scripts/ # build, codegen, lint, and CI utilities