23 Indep nodes keyed by the path into the model

Read and write barriers generated by Xcpp

Consider the following example:


@import "Ceda/cxObject/IObject.h"

$struct X isa ceda::IObject :
    model
    {
        int x;
    }
{
};

Since X implements IObject, Xcpp generates code supporting paths to the fields of the model. It implements assignment to fields of the model in terms of genop_Assign, and read barriers using calls to IndepFieldReadBarrier() are generated:

struct X_model
{
    int x;
};

template <typename BC>
struct X_model_mixin : public BC
{
    using typename BC::FinalClass;

    void EvictDgsNodes() const
    {
        BC::EvictDgsNodes();
        ceda::EvictIndepFieldsUnderObject(static_cast<const typename BC::FinalClass*>(this));
    }

    static void GetxPath(ceda::Path& _path)
    {
        ceda::octet_t* p=_path.InitLocalBuffer(1);
        p[0]=0;
    }
    static ceda::Path GetxPath()
    {
        ceda::Path _path;
        GetxPath(_path);
        return _path;
    }

    int const& Getx() const
    {
        ceda::IndepFieldReadBarrier(static_cast<const typename BC::FinalClass*>(this),GetxPath());
        return _model_.x;
    }
    void Setx(int const& _v)
    {
        ceda::genop_Assign(static_cast<typename BC::FinalClass*>(this),GetxPath(),_model_.x,_v);
    }

    union
    {
        struct
        {
            operator int const&() const { return CONST_ATTRIB_CAST(X_model_mixin,x)->Getx(); }
            int const& read() const { return CONST_ATTRIB_CAST(X_model_mixin,x)->Getx(); }
            ceda::Path path() const { return CONST_ATTRIB_CAST(X_model_mixin,x)->GetxPath(); }
            void operator=(int const& _v) { ATTRIB_CAST(X_model_mixin,x)->Setx(_v); }
        } x;
    };

    X_model const& read() const
    {
        ceda::IndepFieldReadBarrier(static_cast<const typename BC::FinalClass*>(this), ceda::Path());
        return _model_;
    }

    X_model _model_;
};

struct X : public X_model_mixin<ceda::IObjectBaseMixin<X,ceda::EmptyBase>>
{
};

genop_Assign

genop_Assign is defined in IObject.h as follows:


template <typename T>
inline void genop_Assign(ptr<IObject> obj, const Path& path, T& field, const T& val)
{
    AssignValue_params params;
    params.obj = obj;
    params.fid.path = path;
    params.addr = &field;
    params.visitPrefsFn = nullptr;
    OCB_BeforeAssignValue(params);
    field = val;
    OCB_AfterAssignValueWithoutPrefs(params);
}

The DGS write barrier (i.e. IndepFieldWriteBarrier) is invoked in OCB_BeforeAssignValue():


// OperationCallBacks.cpp
@api void OCB_BeforeAssignValue(AssignValue_params& params)
{
    cxAssert(params.addr);
    if (s_operationCallBacks)
    {
        params.fid.oid.low_ = 0;
        params.fid.oid.high_ = 0;
        s_operationCallBacks->OnBeforeAssignValue(params);
    }
    IndepFieldWriteBarrier(params.obj, params.fid.path);
}

s_operationCallBacks->OnBeforeAssignValue(params) does nothing if the object doesn't implement IPersistable.

IObject.h

IObject.h declares the following free functions, these are read/write barriers for an independent field that takes part in the Dependency Graph System (DGS):


@api void EvictIndepFieldsUnderObject(ptr<const IObject> obj);
@api void IndepFieldEvict(ptr<const IObject> obj, const Path& path);
@api void IndepFieldReadBarrier(ptr<const IObject> obj, const Path& path);
@api void IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& path);

// Convenience functions for when we have a path to a model or array and an index into an element of the model or array
@api void IndepFieldReadBarrier(ptr<const IObject> obj, const Path& pathToModelOrArray, ssize_t elementIndex);
@api void IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& pathToModelOrArray, ssize_t elementIndex);

These functions all assume obj is not null and obj->GetReflectedClass() is not null The path identifies the field which is accessed. If the path is empty then DataSourceEvict() evicts all DGS nodes under the given object.

These six free functions are implemented in DGSystem.cpp as follows:


@api void EvictIndepFieldsUnderObject(ptr<const IObject> obj)
{
    cxAssert(obj);
    if (DGSystem* dgs = TryGetThreadDGSystem())
    {
        dgs->EvictIndepFieldsUnderObject(obj);
    }
}

@api void IndepFieldEvict(ptr<const IObject> obj, const Path& path)
{
    cxAssert(obj);
    if (DGSystem* dgs = TryGetThreadDGSystem())
    {
        dgs->IndepFieldEvict(obj,path);
    }
}
@api void IndepFieldReadBarrier(ptr<const IObject> obj, const Path& path)
{
    cxAssert(obj);
    if (DGSystem* dgs = TryGetThreadDGSystem())
    {
        dgs->IndepFieldReadBarrier(obj,path);
    }
}

@api void IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& path)
{
    cxAssert(obj);
    if (DGSystem* dgs = TryGetThreadDGSystem())
    {
        dgs->IndepFieldWriteBarrier(obj,path);
    }
}

@api void IndepFieldReadBarrier(ptr<const IObject> obj, const Path& pathToModelOrArray, ssize_t elementIndex)
{
    ssize_t n = const_cast<Path&>(pathToModelOrArray).Append_ssize_t(elementIndex);
    IndepFieldReadBarrier(obj, pathToModelOrArray);
    const_cast<Path&>(pathToModelOrArray).EraseTail(n);
}

@api void IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& pathToModelOrArray, ssize_t elementIndex)
{
    ssize_t n = const_cast<Path&>(pathToModelOrArray).Append_ssize_t(elementIndex);
    IndepFieldWriteBarrier(obj, pathToModelOrArray);
    const_cast<Path&>(pathToModelOrArray).EraseTail(n);
}

DGSystem.cpp

The corresponding methods on class DGSystem are implemented as follows:


class DGSystem
{
public:
    ///////////////// Indep nodes identified by path under object
    /*
    Used for models for which operations are generated

    These functions all assume obj is not null and obj->GetReflectedClass() is not null
    The path identifies the field which is accessed.
    If the path is empty then DataSourceEvict() evicts all DGS nodes under the given object.
    */
    void DeleteIndepField(ptr<const IObject> obj, const Path& path);
    void EvictIndepFieldsUnderObject(ptr<const IObject> obj);
    void IndepFieldEvict(ptr<const IObject> obj, const Path& path);
    void IndepFieldReadBarrier(ptr<const IObject> obj, const Path& path);
    void IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& path);

private:
    // This is the newer implementation which uses a map keyed by ptr<const IObject> to a
    // tree of indep nodes organised by the path
    DGIndepNodeMap indepFieldNodeMap_;
};

void DGSystem::EvictIndepFieldsUnderObject(ptr<const IObject> obj)
{
    // Independent nodes must not be accessed from OnInvalidate() handler of a dependent node
    cxAssert(m_callback == ECB_None);

    indepFieldNodeMap_.EvictObject(obj);
}

void DGSystem::IndepFieldEvict(ptr<const IObject> obj, const Path& path)
{
    // Independent nodes must not be accessed from OnInvalidate() handler of a dependent node
    cxAssert(m_callback == ECB_None);

    indepFieldNodeMap_.Evict(obj,path);
}

void DGSystem::IndepFieldReadBarrier(ptr<const IObject> obj, const Path& path)
{
    // Independent nodes must not be accessed from OnInvalidate() handler of a dependent node
    cxAssert(m_callback == ECB_None);

    if (m_currentNodeBeingCalculated)
    {
        indepFieldNodeMap_.ReadBarrier(obj,path);
    }
}

void DGSystem::IndepFieldWriteBarrier(ptr<const IObject> obj, const Path& path)
{
    // Independent nodes must not be accessed from OnInvalidate() handler of a dependent node
    cxAssert(m_callback == ECB_None);

    // This assertion trips if the programmer has tried to modify a model while recalculating
    // a node
    cxAssert(!m_currentNodeBeingCalculated);

    indepFieldNodeMap_.WriteBarrier(obj,path);
}

Need for reflection

The current approach assumes the class of the given object (pointed at by a ptr<IObject>) is reflected. That is necessary to be able to deserialise the path. This is particularly when we get to a key of a map.

If we don't build a tree of nodes under an object we don't get the idea of read barriers on intermediate nodes.

Without reflection we don't get the ability to trace friendly names of fields that take part in the DGS.

So there are all these good reasons to want to assume the object is reflected. But for example JigsawView has a model and it is not reflected.

Code like this is generated


static void GetTranslateyPath(ceda::Path& _path) { ceda::octet_t* p=_path.InitLocalBuffer(2); p[0]=1; p[1]=1; }
static ceda::Path GetTranslateyPath() { ceda::Path _path; GetTranslateyPath(_path); return _path; }
ceda::float64 const& _model__Translatey() const { return _model_.Translate.y; }
ceda::float64& _model__Translatey() { return _model_.Translate.y; }
ceda::float64 const& GetTranslatey() const { ceda::IndepFieldReadBarrier(static_cast<const FinalClass*>(this),GetTranslateyPath()); return _model_.Translate.y; }
void SetTranslatey(ceda::float64 const& _v) { ceda::genop_Assign(static_cast<FinalClass*>(this),GetTranslateyPath(),_model_.Translate.y,"JigsawView::_model_.Translate.y",_v); }

genop_Assign calls OCB_BeforeAssignValue which calls IndepFieldWriteBarrier(params.obj, params.fid.path).

Let's forget about making sure we can print nice names. We are more interested in very efficient, general solution. If it can be independent of reflection then so much the better.

IDEA: Let's assume we generate some basic level of reflection on any class which has a model.

<<reflect -dc -copy>> has to be specified on JigsawView. Maybe this should be like the default on views with models.

Minimal 'reflection' to support tree of nodes

A path is defined regardless of the use of platform dependent types, or generic types in a template. However we currently have a problem with the reflection. But we would still like to create a tree of nodes.

It is proposed they we introduce a lightweight system to support this. Basically we need what the ReflectedClass currently gives us: a vector of model fields. For each model field we want the name (to allow a path to be printed) and byte code which can be used to help process a path.

DGIndepNodeMap

This is basically a map from ptr<const IObject> to a pointer to the root of a tree of DgiNode nodes.

Implemented in DGIndep.cpp/h

class DGIndepNodeMap
{
public:
    DGIndepNodeMap();
    ~DGIndepNodeMap();
    void VisitObjects(IObjectVisitor& v) const;

    bool empty() const { return map_.empty(); }

    void EvictObject(ptr<const IObject> obj);

    // Call TryEvict() on the DGIndepNodeForModelField identified by the given 'obj' and 'path'
    // Returns false if there is no such DGIndepNodeForModelField
    bool Evict(ptr<const IObject> obj, const Path& path);

    // Called as a result of the callback DGIndepNodeForModelField::OnEvict()
    bool DeleteIndepField(ptr<const IObject> obj, const Path& path);

    // Called when a read barrier is invoked on the field with the given path under the given
    // object.  Note that the path doesn't necessarily go all the way down to the fields which
    // are the target of operations.
    void ReadBarrier(ptr<const IObject> obj, const Path& path);

    // Called when an operation is generated on the field with the given path under the given
    // object
    void WriteBarrier(ptr<const IObject> obj, const Path& path);

private:
    DgiNode** ProcessVecNodeForReadBarrier(DgiNode** pCurrentNode, PathNav::Element e);
    bool ProcessNodeForWriteBarrier(DgiNode* node);
    DgiNode* ProcessVecNodeForWriteBarrier(DgiNode* node, PathNav::Element e);

    // During calls to EvictObject() and Evict() this flag is set, so when DeleteIndepField() is
    // called as a result of the callback DGIndepNodeForModelField::OnEvict(), nothing is done.
    // This allows EvictObject() and Evict() to take on responsibility for deleting the node(s)
    // which is more efficient.
    bool disableDeleteIndepField_;

    // For objects that have models and for which a read barrier has been invoked on at least
    // one of the fields "under" it.
    // The key and mapped values of each element of the map are never null.
    typedef std::map< ptr<const IObject>, DgiNode*> MAP;
    MAP map_;
};

Operation notifications

todo: this implementation is ideal for providing operation notifications (see OperationNotifications.h which currently only supports vectors).

The tree of indep nodes can also be used to record "observers" that have attached in order to be notified of operation events.

Indeed maybe the DGS should hook into a notification system!

There is a mixin with <<genop>> on it because we must support operation notifications.


    $mixin TransientTextBoxMixin <<genop>> :
        model
        {
            xstring Text;
        }
    {
        ...
    };

That means we want genop_xxx functions to be called. The mixin is used to define a templatised concrete class.


    $struct EditBoxOnAssignableStringFieldOfModel<typename T>  isa IEditTextCtrlModel :
        mixin
        [
            TransientTextBoxMixin
            ViewModelBaseMixin
            EditTextCtrlModelBaseMixin
        ]
    {
        ...
    }

So we need operation notifications on template classes. But template classes are not reflected.

The interface for operation notifications is great for hooking into system wide notifications but not really appropriate for getting notifications on a given object (say). We would like to be able to attach observers at any node in the tree of nodes.


    $typedef+ void* FieldAddress;

    $interface+ IVectorFieldNotifications
    {
        void OnVectorInsert(bool localOp, ptr<IObject> obj, const Path& path, FieldAddress fa, ssize_t i1, ssize_t i2);
        void OnVectorErase(bool localOp, ptr<IObject> obj, const Path& path, FieldAddress fa, ssize_t i1, ssize_t i2);
    };

    @api void RegisterForVectorFieldNotifications(ptr<IVectorFieldNotifications> vfn);
    @api void UnregisterForVectorFieldNotifications(ptr<IVectorFieldNotifications> vfn);