diff --git a/conf.py b/conf.py
index bc201b8b..1cb86062 100644
--- a/conf.py
+++ b/conf.py
@@ -17,7 +17,7 @@ author = 'Roland Conybeare'
extensions = [ "breathe",
"sphinx.ext.mathjax", # inline math
"sphinx.ext.autodoc", # generate info from docstrings
-# "sphinxcontrib.ditaa", # diagrams-through-ascii-art
+ "sphinxcontrib.ditaa", # diagrams-through-ascii-art
"sphinxcontrib.plantuml", # text -> uml diagrams
]
diff --git a/xo-cmake/cmake/xo_macros/xo_cxx.cmake b/xo-cmake/cmake/xo_macros/xo_cxx.cmake
index 261a899c..2e4ed18e 100644
--- a/xo-cmake/cmake/xo_macros/xo_cxx.cmake
+++ b/xo-cmake/cmake/xo_macros/xo_cxx.cmake
@@ -471,7 +471,25 @@ macro(xo_docdir_sphinx_config rst_files)
if (XO_ENABLE_DOCS)
if (XO_SUBMODULE_BUILD)
- # in submodule build, rely on toplevel docs/CMakeLists.txt file instead
+ # in submodule build, rely on toplevel docs/CMakeLists.txt file instead.
+ #
+ # translate ${rst_files} to absolute paths
+ #
+ set(SPHINX_ABS_RST_FILES)
+ foreach(rst_file ${SPHINX_RST_FILES})
+ get_filename_component(
+ abs_path "${rst_file}"
+ ABSOLUTE
+ BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ list(APPEND SPHINX_ABS_RST_FILES "${abs_path}")
+ endforeach()
+
+ # append to global property
+ set_property(GLOBAL APPEND
+ PROPERTY XO_UMBRELLA_SPHINX_RST_FILES
+ ${SPHINX_ABS_RST_FILES})
+
+ message(STATUS "SPHINX_ABS_RST_FILES=${SPHINX_ABS_RST_FILES}")
else()
# build docs starting from here only in standalone build.
# otherwise use top-level doxygen setup.
@@ -529,6 +547,7 @@ endmacro()
# config for an umbrella project that composes standalone subprojects
#
macro(xo_umbrella_sphinx_config rst_files)
+ # here SPHINX_RST_FILES refers to toplevel-only .rst files in umbrella project
list(APPEND SPHINX_RST_FILES ${rst_files})
foreach(arg IN ITEMS ${ARGN})
list(APPEND SPHINX_RST_FILES ${arg})
@@ -539,12 +558,17 @@ macro(xo_umbrella_sphinx_config rst_files)
find_program(SPHINX_EXECUTABLE NAMES sphinx-build REQUIRED)
message(STATUS "SPHINX_EXECUTABLE=${SPHINX_EXECUTABLE}")
+ get_property(SPHINX_ABS_RST_FILES GLOBAL PROPERTY XO_UMBRELLA_SPHINX_RST_FILES)
+ message(STATUS "SPHINX_ABS_RST_FILES=${SPHINX_ABS_RST_FILES}")
+
set(SPHINX_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/sphinx/html)
set(SPHINX_INDEX_FILE ${SPHINX_OUTPUT_DIR}/index.html)
# root of sphinx doc tree
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
- set(SPHINX_DEPS doxygen_${PROJECT_NAME} conf.py ${SPHINX_RST_FILES} ${SPHINX_RST_FILES_GLOB} ${DOX_DEPS})
+ # SPHINX_RST_FILES: top-level .rst files in umbrella project
+ # SPHINX_ABS_RST_FILES: satellite .rst files, collected via XO_SUBMODULE_BUILD, rewritten to absoluate paths
+ set(SPHINX_DEPS doxygen_${PROJECT_NAME} conf.py ${SPHINX_RST_FILES} ${SPHINX_ABS_RST_FILES} ${DOX_DEPS})
add_custom_command(
OUTPUT ${SPHINX_INDEX_FILE}
diff --git a/xo-facet/docs/CMakeLists.txt b/xo-facet/docs/CMakeLists.txt
index 0e072222..4a052914 100644
--- a/xo-facet/docs/CMakeLists.txt
+++ b/xo-facet/docs/CMakeLists.txt
@@ -7,7 +7,7 @@ xo_docdir_sphinx_config(
glossary.rst
#install.rst
#introduction.rst
- #implementation.rst
+ implementation.rst
)
# see xo-reader/doc or xo-unit/doc for working examples
diff --git a/xo-facet/docs/glossary.rst b/xo-facet/docs/glossary.rst
index 73dd5ea3..9703e5ee 100644
--- a/xo-facet/docs/glossary.rst
+++ b/xo-facet/docs/glossary.rst
@@ -6,3 +6,6 @@ Glossary
.. glossary::
fomo
| facet object model
+
+ xfer
+ | abbreviation for transfer
diff --git a/xo-facet/docs/implementation.rst b/xo-facet/docs/implementation.rst
new file mode 100644
index 00000000..92891c9e
--- /dev/null
+++ b/xo-facet/docs/implementation.rst
@@ -0,0 +1,93 @@
+.. _implementation:
+
+Components
+==========
+
+Library dependency tower for *xo-facet*:
+
+.. ditaa::
+
+ +-----------------+
+ | xo_facet |
+ +-----------------+
+ | xo_cmake |
+ +-----------------+
+
+Abstraction tower for *xo-facet* components.
+
+.. ditaa::
+ :--scale: 0.85
+
+ +--------------------------------+
+ | obj(A,D) |
+ +--------------------------------+
+ | RRouter(A,D) |
+ +--------------------------------+
+ | OObject(A,D) |
+ +--------------------------------+
+ | FacetImplmentationType(A,D) |
+ +----------------+---------------+
+ | facet [A] | data [D] |
+ +----------------+---------------+
+
+
+Decorated with sample method calls, to reveal type recovery
+
+.. ditaa::
+ :--scale: 0.85
+
+ +--------------------------------+
+ | obj(A,D) | x.foo()
+ +--------------------------------+
+ | RRouter(A,D) | x.foo()
+ +--------------------------------+
+ | OObject(A,D) | x.iface_.foo(x.data_)
+ +--------------------------------+
+ | FacetImplmentationType(A,D) | x.foo(void*data)
+ +----------------+---------------+
+ | facet A | data D | virtual x.foo(void* data)
+ +----------------+---------------+
+
+.. list-table:: Descriptions
+ :header-rows: 1
+ :widths: 30 30 60
+
+ * - Component
+ - Use
+ - Description
+ * - obj
+ - x.foo()
+ - convenience wrapper with interface A, with state D*
+ * - RRouter
+ - x.foo()
+ - auto injects data pointer
+ * - OObject
+ - x.iface()->foo(x.data(), ..)
+ - fat object pointer. combine i/face + data pointer.
+ * - FacetImplementationType
+ - x.foo(void* data, ..)
+ - implement facet for a particular state datatype;
+ explicit type-erased state
+ * - facet
+ - x.foo(void* data, ..)=0
+ - fully abstract interface; explicit type-erased state
+
+.. uml::
+ :caption: fat-object-pointer layout
+ :scale: 99%
+ :align: center
+
+ object z1<>
+ z1 : iface = vt1
+ z1 : data = d1
+
+ object vt1<>
+ vt1 : foo()
+ vt1 : bar()
+
+ object d1<>
+ d1 : x = 0.6
+ d1 : y = 0.8
+
+ z1 o-- vt1
+ z1 o-- d1
diff --git a/xo-facet/docs/index.rst b/xo-facet/docs/index.rst
index bc230b00..68bba28a 100644
--- a/xo-facet/docs/index.rst
+++ b/xo-facet/docs/index.rst
@@ -49,6 +49,7 @@ This gives us several benefits:
:maxdepth: 2
:caption: xo-facet contents
+ implementation
glossary
genindex
search
diff --git a/xo-facet/include/xo/facet/OObject.hpp b/xo-facet/include/xo/facet/OObject.hpp
new file mode 100644
index 00000000..7c87a66f
--- /dev/null
+++ b/xo-facet/include/xo/facet/OObject.hpp
@@ -0,0 +1,193 @@
+/** @file OObject.hpp
+ *
+ * @author Roland Conybeare, Dec 2025
+ **/
+
+#pragma once
+
+#include "facet_implementation.hpp"
+#include "typeseq.hpp"
+#include
+#include
+
+namespace xo {
+ namespace facet {
+ template
+ consteval bool valid_object_traits()
+ {
+ static_assert(requires { typename OObject::AbstractInterface; },
+ "OObject type must provide typename Object::AbstractInterface");
+ static_assert(requires { typename OObject::ISpecific; },
+ "OObject type must provide typename Object::ISpecific");
+ static_assert(requires { typename OObject::DataType; },
+ "OObject type must provide typename Object::DataType");
+ static_assert(valid_facet_implementation,
+ "OObject::ISpecific must implement Object::AbstractInterface");
+ static_assert(std::is_standard_layout_v,
+ "OObject must have standard layout, i.e. no virtual methods."
+ " Virtual methods belong in OObject::AbstractInterface");
+ static_assert(requires(const OObject & obj) {
+ { obj.iface() } -> std::convertible_to; },
+ "OObject must have non-virtual method iface()"
+ " returning const OObject::AbstractInterface");
+ static_assert(requires(const OObject & obj) {
+ { obj.data() } -> std::convertible_to; },
+ "OObject must have non-virtual method data() returning OObject::DataType*");
+
+ return true;
+ }
+
+ /** A "fat object pointer": combines two pointers:
+ *
+ * 1. behavior: an interface pointer
+ * (implementation of @tparam AFacet a.k.a. vtable pointer)
+ * for passive state DRepr.
+ * Interface pointers are static globals.
+ *
+ * 2. state: a data pointer to instance of passive state DRepr
+ * An OObject instance does not own its data pointer
+ *
+ * Performance note: when DRepr can be determined at compile time,
+ * it's often feasible to optimize away the interface part.
+ *
+ * Runtime polymorphism when @tparam DRepr is @ref DVariantPlaceholder
+ *
+ * Application code should not use this directly.
+ * Instead, inherit a facet-specific routing wrapper that automatically
+ * injects @ref data as first argument to @ref iface_ methods.
+ **/
+ template
+ struct OObject {
+ using FacetType = AFacet;
+ using ISpecific = FacetImplType;
+ using DataType = DRepr;
+ using DataPtr = DRepr*;
+
+ explicit OObject() {}
+ explicit OObject(DataPtr d) : data_{d} {}
+
+ /** trivial: nothing to do for @ref iface_ and does not own @ref data_ **/
+ ~OObject() = default;
+
+ /** OObject is truthy **/
+ operator bool() const { return data_ != nullptr; }
+
+ /** interface pointer for variant OObject instances.
+ * These instance support runtime polymorphism.
+ **/
+ const FacetType * iface() const
+ requires std::is_same_v
+ {
+ /* std::launder:
+ *
+ * contents of iface_ at runtime will not match
+ * compile-time datatype. This prohibits compiler de-virtualizing
+ * calls to ISpecific methods, based on mistaken belief that
+ * vtable pointer is known at compile time.
+ */
+ return std::launder(&iface_);
+ }
+
+ /** interface pointer for OObject instance with representation
+ * known at compile time.
+ *
+ * Calls here should be straightforward to devirtualize
+ **/
+ const FacetType * iface() const
+ requires (!std::is_same_v)
+ {
+ /* don't use std::launder: want compiler to devirtualize
+ * calls to virtual @ref iface_ methods
+ */
+ return &iface_;
+ }
+
+ DataPtr data() const { return data_; }
+
+ void reset() { data_ = nullptr; }
+
+ /**
+ * We're either:
+ * - assigning from pointer with compatible representation
+ * - implementing the fat-object-pointer equivalent of
+ * assigning a derived pointer to a base pointer.
+ **/
+ template
+ OObject & from_data(DOther * other) {
+ static_assert(std::is_same_v
+ || std::is_convertible_v);
+
+ if constexpr (std::is_convertible_v) {
+ /** assigning from data with same representation **/
+ this->data_ = other;
+ } else /*DRepr is DVariantPlaceholder*/ {
+ /** assigning to variant **/
+
+ /* acquire fat object pointer for (AFacet, DOther) */
+ OObject oother(other);
+
+ static_assert(sizeof(*this) == sizeof(oother));
+
+ ::memcpy((void*)this, (void*)&oother, sizeof(*this));
+ }
+
+ return *this;
+ }
+
+ /**
+ * Downcast to pointer of type DOther*, if valid.
+ * Provided when actual type of @ref data_ is not DRepr,
+ * because DRepr is DVariantPlaceholder.
+ *
+ * We can't rely on dynamic_cast here, because DRepr's
+ * don't need to be related as far as c++ type system is
+ * concerned.
+ **/
+ template
+ DOther * downcast()
+ requires (std::is_same_v)
+ {
+ if (data_ && (typeseq::id() == this->iface()->_typeseq())) {
+ /* actual runtime type for data_ is DOther,
+ * safe to reinterpret
+ */
+ return reinterpret_cast(data_);
+ } else {
+ return nullptr;
+ }
+ }
+
+ DRepr & operator*() { return *data_; }
+
+#ifdef NOT_IN_USE
+ // not sure if this is a good idea. could just as well write
+ // auto obj = ...;
+ // *obj.data() == rhs
+
+ /** assign contents of rhs in-place **/
+ OObject & operator=(const DRect & rhs) {
+ assert(data_);
+
+ *(this->data_) = rhs;
+
+ return *this;
+ }
+#endif
+
+ /** fetch data pointer. load-bearing for routing classes **/
+ static bool _valid;
+
+ /** runtime interface for this object **/
+ ISpecific iface_;
+ /** runtime state for this object **/
+ DataPtr data_ = nullptr;
+ };
+
+ template
+ bool
+ OObject::_valid = valid_object_traits();
+
+ } /*namespace facet*/
+} /*namespace xo*/
+
+/* end OObject.hpp */
diff --git a/xo-facet/include/xo/facet/OUniqueBox.hpp b/xo-facet/include/xo/facet/OUniqueBox.hpp
new file mode 100644
index 00000000..d22ad70f
--- /dev/null
+++ b/xo-facet/include/xo/facet/OUniqueBox.hpp
@@ -0,0 +1,74 @@
+/** @file OUniqueBox.hpp
+ *
+ * @author Roland Conybeare, Dec 2025
+ **/
+
+namespace xo {
+ namespace facet {
+ /**
+ * Uniquely-owned instance with runtime polymorphism.
+ *
+ * Reminder that in the facet object model we expect
+ * objects to be transient.
+ *
+
+ *
+ * Unlike OUniqueBox can use for variant data
+ * without additional overhead. Tradeoff is that avoiding such
+ * overhead excludes std::unique_ptr.
+ *
+ * We're going to instead rely on AInterface providing a destruct_data() method,
+ * so in practice get the deleter from interface state.
+ *
+ * Possibly means we need all abstract interfaces to share a common base
+ *
+ * Remarks:
+ * - when @tparam Data is supplied
+ **/
+ template
+ struct OUniqueBox {
+ using AbstractInterface = AInterface;
+ using ISpecific = ISpecificFor::ImplType;
+ /* note: Data can be void here */
+ using DataType = Data;
+ using DataBox = Data*;
+
+ explicit OUniqueBox() {}
+ /* unsatisfactory b/c doesn't enforce that @p d is heap-allocated */
+ explicit OUniqueBox(DataBox d) : data_{std::move(d)} {}
+
+ ~OUniqueBox() {
+ if (data_ != nullptr) {
+ this->iface()->destruct_data(data_);
+ delete data_;
+ this->data_ = nullptr;
+ }
+ }
+
+ const AInterface * iface() const
+ requires std::is_same_v
+ {
+ return std::launder(&iface_);
+ }
+
+ const AInterface * iface() const
+ requires (!std::is_same_v)
+ {
+ return &iface_;
+ }
+
+ /** note: would prefer this to be constexpr, but not simple asof gcc 14.3 **/
+ static bool _valid;
+
+ /** note: load-bearing for routing classes such as RComplex **/
+ Data * data() const { return data_; }
+
+ ISpecific iface_;
+ DataBox data_ = nullptr;
+ };
+
+
+ }
+} /*namespace xo*/
+
+/* end OUniqueBox.hpp */
diff --git a/xo-facet/include/xo/facet/RRouter.hpp b/xo-facet/include/xo/facet/RRouter.hpp
new file mode 100644
index 00000000..daa7887c
--- /dev/null
+++ b/xo-facet/include/xo/facet/RRouter.hpp
@@ -0,0 +1,51 @@
+/** @file RRouter.hpp
+ *
+ * @author Roland Conybeare, Dec 2025
+ **/
+
+#pragma once
+
+#include "OObject.hpp"
+
+namespace xo {
+ namespace facet {
+ template
+ consteval bool valid_object_router()
+ {
+ static_assert(requires { typename RRouter::ObjectType; },
+ "Router type must provide typename Router::ObjectType");
+ static_assert(valid_object_traits,
+ "Router::ObjectType must satisfy objectmodel traits");
+ static_assert(std::is_standard_layout_v,
+ "Router must have standard laayout, i.e. no virtual methods."
+ " Virtual methods belong in OObject::AbstractInterface*>");
+ return true;
+ };
+
+ /**
+ * template
+ * struct RMyFacet : public Object {
+ * using ObjectType = Object;
+ *
+ * RObject() = default;
+ * RObject(Object::DataPtr data) : Object{data} {}
+ *
+ * void something() const { return Object::iface()->something(Object::data()); }
+ * int andalso(double somearg) const { return Object::iface()->andalso(Object::data(), somearg); }
+ * };
+ *
+ * template
+ * struct RoutingFor {
+ * using RoutingType = RMyFacet