From 04ca79fd65c252615610a5fbf0641cfbc907d535 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sat, 25 Nov 2023 03:36:19 +0100 Subject: [PATCH] Chain-Load: verify re-initialisation and copy ...this is a more realistic demo example, which mimics some of the patterns present in RandomDraw. The test also uses lambdas linking to the actual storage location, so that the invocation would crash on a copy; LazyInit was invented to safeguard against this, while still allowing leeway during the initialisation phase in a DSL. --- src/lib/lazy-init.hpp | 79 ++++++++++---- tests/library/lazy-init-test.cpp | 117 ++++++++++++++++++--- wiki/thinkPad.ichthyo.mm | 172 +++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+), 36 deletions(-) diff --git a/src/lib/lazy-init.hpp b/src/lib/lazy-init.hpp index 3fdbc5592..4d3107a48 100644 --- a/src/lib/lazy-init.hpp +++ b/src/lib/lazy-init.hpp @@ -54,7 +54,7 @@ ** that the »trojan functor« itself is stored somehow embedded into the target object ** to be initialised. If there is a fixed distance relation in memory, then the target ** can be derived from the self-position of the functor; if this assumption is broken - ** however, memory corruption and SEGFAULT may be caused. + ** however, memory corruption and SEGFAULT may be caused. ** ** @todo 11/2023 at the moment I am just desperately trying to get a bye-product of my ** main effort into usable shape and salvage an design idea that sounded clever @@ -73,15 +73,12 @@ #define LIB_LAZY_INIT_H -//#include "lib/error.h" -//#include "lib/nocopy.hpp" +#include "lib/error.h" #include "lib/meta/function.hpp" #include "lib/opaque-holder.hpp" -//#include "lib/meta/function-closure.hpp" -//#include "lib/util-quant.hpp" #include "lib/util.hpp" -//#include +#include #include #include @@ -90,21 +87,23 @@ namespace lib { namespace err = lumiera::error; using lib::meta::_Fun; + using lib::meta::_FunArg; using lib::meta::has_Sig; -// using std::function; using util::unConst; + using std::function; using std::forward; using std::move; using RawAddr = void const*; + namespace {// the anonymous namespace of horrors... inline ptrdiff_t captureRawAddrOffset (RawAddr anchor, RawAddr subject) { // Dear Mr.Compiler, please get out of my way. - // I just sincerely want to shoot myself into my foot... + // I just genuinely want to shoot myself into my foot... char* anchorAddr = reinterpret_cast (unConst(anchor)); char* subjectAddr = reinterpret_cast (unConst(subject)); return subjectAddr - anchorAddr; @@ -140,7 +139,10 @@ namespace lib { "apply small-object optimisation with inline storage."}; return captureRawAddrOffset (functor,payload); }(); - } + // + }//(End)low-level manipulations + + /** @@ -171,12 +173,11 @@ namespace lib { * and then to forward the invocation to the actual * function, which should have been initialised * by the delegate invoked. - * @param delegate a functor object pass invocation; + * @param delegate a functor object to forward invocation; * the delegate must return a reference to the * actual function implementation to invoke. * Must be heap-allocated. * @return a lightweight lambda usable as trigger. - * @note takes ownership of the delegate */ template static auto @@ -195,10 +196,18 @@ namespace lib { - /** - * + + + struct EmptyBase { }; + + /**************************************************************//** + * Mix-in for lazy/delayed initialisation of an embedded functor. + * This allows to keep the overall object (initially) copyable, + * while later preventing copy once the functor was »engaged«. + * Initially, only a »trap« is installed into the functor, + * invoking an initialisation closure on first use. */ - template + template class LazyInit : public PAR { @@ -209,8 +218,10 @@ namespace lib { using HeapStorage = InPlaceBuffer; using PendingInit = std::shared_ptr; + /** manage heap storage for a pending initialisation closure */ PendingInit pendingInit_; + PendingInit const& __trapLocked (PendingInit const& init) { @@ -232,13 +243,27 @@ namespace lib { } + + protected: + struct MarkDisabled{}; + + /** @internal allows derived classes to leave the initialiser deliberately disabled */ + template + LazyInit (MarkDisabled, ARGS&& ...parentCtorArgs) + : PAR(forward (parentCtorArgs)...) + , pendingInit_{} + { } + + public: + /** prepare an initialiser to be activated on first use */ template LazyInit (std::function& targetFunctor, INI&& initialiser, ARGS&& ...parentCtorArgs) : PAR(forward (parentCtorArgs)...) , pendingInit_{prepareInitialiser (targetFunctor, forward (initialiser))} { } + LazyInit (LazyInit const& ref) : PAR{ref} , pendingInit_{__trapLocked (ref.pendingInit_)} @@ -272,6 +297,12 @@ namespace lib { } + bool + isInit() const + { + return not pendingInit_; + } + template void installEmptyInitialiser() @@ -279,7 +310,15 @@ namespace lib { pendingInit_.reset (new HeapStorage{emptyInitialiser()}); } - private: + template + void + installInitialiser (std::function& targetFunctor, INI&& initialiser) + { + pendingInit_ = prepareInitialiser (targetFunctor, forward (initialiser)); + } + + + private: /* ========== setup of the initialisation mechanism ========== */ template DelegateType emptyInitialiser() @@ -306,7 +345,7 @@ namespace lib { template DelegateType* - getPointerToDelegate(HeapStorage& buffer) + getPointerToDelegate (HeapStorage& buffer) { return reinterpret_cast*> (&buffer); } @@ -316,6 +355,7 @@ namespace lib { buildInitialiserDelegate (std::function& targetFunctor, INI&& initialiser) { using TargetFun = std::function; + using ExpectedArg = _FunArg; return DelegateType{ [performInit = forward (initialiser) ,targetOffset = captureRawAddrOffset (this, &targetFunctor)] @@ -324,9 +364,10 @@ namespace lib { TargetFun* target = relocate (location, -FUNCTOR_PAYLOAD_OFFSET); LazyInit* self = relocate (target, -targetOffset); REQUIRE (self); - performInit (self); - self->pendingInit_.reset(); - return *target; + // invoke init, possibly downcast to derived *self + performInit (static_cast (self)); + self->pendingInit_.reset(); // release storage + return *target; // invoked by the »Trojan« to yield first result }}; } }; diff --git a/tests/library/lazy-init-test.cpp b/tests/library/lazy-init-test.cpp index 002bf2d8b..da13eb8f6 100644 --- a/tests/library/lazy-init-test.cpp +++ b/tests/library/lazy-init-test.cpp @@ -34,7 +34,7 @@ #include "lib/test/diagnostic-output.hpp" /////////////////////TODO TODOH #include "lib/util.hpp" -//#include +#include @@ -42,10 +42,10 @@ namespace lib { namespace test{ // using util::_Fmt; + using std::make_unique; using util::isSameObject; using lib::meta::isFunMember; -// using lib::meta::_FunRet; -// using err::LUMIERA_ERROR_LIFECYCLE; + using err::LUMIERA_ERROR_LIFECYCLE; @@ -78,6 +78,7 @@ namespace test{ verify_TargetRelocation(); verify_triggerMechanism(); verify_lazyInitialisation(); + verify_complexUsageWithCopy(); } @@ -87,7 +88,7 @@ namespace test{ * # the _target function_ finally to be invoked performs a verifiable computation * # the _delegate_ receives an memory location and returns a reference to the target * # the generated _»trojan λ«_ captures its own address, invokes the delegate, - * retrieves a reference to a target functor, and invokes these with actual arguments. + * retrieves a reference to a target functor, and finally invokes this with actual arguments. * @remark the purpose of this convoluted scheme is for the _delegate to perform initialisation,_ * taking into account the current memory location „sniffed“ by the trojan. */ @@ -119,16 +120,15 @@ namespace test{ return fun; }; using Delegate = decltype(delegate); - Delegate *delP = new Delegate(delegate); + auto delP = make_unique (delegate); // verify the heap-allocated copy of the delegate behaves as expected location = nullptr; CHECK (beacon+c == (*delP)(this)(c)); CHECK (location == this); - // now (finally) build the »trap function«, - // taking ownership of the heap-allocated delegate copy - auto trojanLambda = TrojanFun::generateTrap (delP); + // now (finally) build the »trap function«... + auto trojanLambda = TrojanFun::generateTrap (delP.get()); CHECK (sizeof(trojanLambda) == sizeof(size_t)); // on invocation... @@ -138,7 +138,7 @@ namespace test{ CHECK (beacon+c == trojanLambda(c)); CHECK (location == &trojanLambda); - // repeat that with a copy, and changed beacon value + // repeat same with a copy, and changed beacon value auto trojanClone = trojanLambda; beacon = rand(); c = beacon % 55; @@ -150,17 +150,17 @@ namespace test{ - /** @test verify that std::function indeed stores a simple functor inline + /** @test verify that std::function indeed stores a simple functor inline. * @remark The implementation of LazyInit relies crucially on a known optimisation * in the standard library ─ which unfortunately is not guaranteed by the standard: * Typically, std::function will apply _small object optimisation_ to place a very * small functor directly into the wrapper, if the payload has a trivial copy-ctor. - * Libstdc++ is known to be rather restrictive, other implementations trade increased - * storage size of std::function against more optimisation possibilities. + * `Libstdc++` is known to be rather restrictive, while other implementations trade + * increased storage size of std::function against more optimisation possibilities. * LazyInit exploits this optimisation to „spy“ about the current object location, - * to allow executing the lazy initialisation on first use, without further help + * allowing to execute the lazy initialisation on first use, without further help * by client code. This trickery seems to be the only way, since λ-capture by reference - * is broken after copying or moving the host object (which is required for DSL use). + * is broken after copying or moving the host object (typically required for DSL use). * In case this turns out to be fragile, LazyInit should become a "LateInit" and needs * help by the client or the user to trigger initialisation; alternatively the DSL * could be split off into a separate builder object distinct from RandomDraw. @@ -169,7 +169,7 @@ namespace test{ verify_inlineStorage() { // char payload[24];// ◁─────────────────────────────── use this to make the test fail.... - const char* payload = "Outer Space"; + const char* payload = "please look elsewhere"; auto lambda = [payload]{ return RawAddr(&payload); }; RawAddr location = lambda(); @@ -196,7 +196,7 @@ namespace test{ * by applying known offsets consecutively * from a starting point within an remote instance * @remark in the real usage scenario, we know _only_ the offset - * and and attempt to find home without knowing the layout. + * and attempt to find home without knowing the layout. */ void verify_TargetRelocation() @@ -229,7 +229,7 @@ namespace test{ CHECK (offNested > 0); // create a copy far far away... - auto farAway = std::make_unique (here); + auto farAway = make_unique (here); // reconstruct base address from starting point RawAddr startPoint = farAway->peek(); @@ -322,6 +322,89 @@ namespace test{ CHECK (1 == invoked); CHECK (init); } + + + + /** elaborate setup used for integration test */ + struct LazyDemo + : LazyInit<> + { + using Fun = std::function; + + int seed{0}; + Fun fun; // ◁────────────────────────────────── this will be initialised lazily.... + + template + auto + buildInit (FUN&& fun2install) + { + return [theFun = forward (fun2install)] + (LazyDemo* self) + { + CHECK (self); + self->fun = [self, chain = move(theFun)] + (int i) + { + return chain (i + self->seed); // Note: binding to actual instance location + }; + }; + } + + + LazyDemo() + : LazyInit{MarkDisabled()} + , fun{} + { + installInitialiser(fun, buildInit([](int){ return 0; })); + } + + template + LazyDemo(FUN&& someFun) + : LazyInit{MarkDisabled()} + , fun{} + { + installInitialiser(fun, buildInit (forward (someFun))); + } + }; + + /** + * @test use an elaborately constructed example to cover more corner cases + * - the function to manage and initialise lazily is _a member_ of the _derived class_ + * - the initialisation routine _adapts_ this function and links it with the current + * object location; thus, invoking this function on a copy would crash / corrupt memory. + * - however, as long as initialisation has not been triggered, LazyDemo instances can be + * copied; they may even be assigned to existing instances, overwriting their state. + */ + void + verify_complexUsageWithCopy() + { + LazyDemo d1; + CHECK (not d1.isInit()); // not initialised, since function was not invoked yet + CHECK (d1.fun); // the functor is not empty anymore, since the »trap« was installed + + d1.seed = 2; + CHECK (0 == d1.fun(22)); // d1 was default initialised and thus got the "return 0" function + CHECK (d1.isInit()); // first invocation also triggered the init-routine + + // is »engaged« after init and rejects move / copy + VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} ); + + + d1 = LazyDemo{[](int i) // assign a fresh copy (discarding any state in d1) + { + return i + 1; // using a "return i+1" function + }}; + CHECK (not d1.isInit()); + CHECK (d1.seed == 0); // assignment indeed erased any existing settings (seed≔2) + CHECK (d1.fun); + + CHECK (23 == d1.fun(22)); // new function was tied in (while also referring to self->seed) + CHECK (d1.isInit()); + d1.seed = 3; // set the seed + CHECK (26 == d1.fun(22)); // seed value is picked up dynamically + + VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} ); + } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 8e62e33f3..0fff39895 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -97280,6 +97280,93 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ die Funktion ist dieses Mal ein Feld im abgeleiteten Objekt (yess!) +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -97451,12 +97538,97 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ hier besteht ein latentes semantsiches Problem: +

+

+ lazyInit ⟹ Objekt ist erst mal noch nicht ganz initialisiert +

+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ dieses Design ist MIST +

+ + +
+
+
+
+
+