Back on "message after tag delete" post

Hi there,

I'm looking for a reliable method to know when a TAG is deleted. I was hoping that a destructor exists somehow, it it does not work as expected. I tried __del__(self) as well as NodeData.Free(self, node) in my class. Both are called on TAG deletion, but they are also called on any user interaction, which looks really weird.

I went through old post, https://plugincafe.maxon.net/topic/7838/10131_message-after-tag-delete?_=1674474478232, which state exactly the same result without any solution.

Is there a way to know when a TAG has been deleted or destroyed from an object without having to watch constantly the whole hierarchy?

Thanks a lot,
Cheers,

Christophe

Hello @mocoloco,

Thank you for reaching out to us. The behavior you are running into is caused by the Asset API. There are many facets to this problem of node reallocation.

Node Reallocation

Cinema 4D always has reallocated nodes much more frequently than it might appear to be the case for the user. But with the introduction of the Asset API, this behavior has increased significantly, which is why I then added this comment to NodeData::Init both in Python and C++:

NodeData.Init is being called frequently due to nodes being reallocated in the background by Cinema 4D. One should therefore be careful with carrying out computationally complex tasks in this function.

This also applies by extension to NodeData::Free. The underlying cause is that the Asset API is initializing and freeing nodes to manage presets. So, your plugin hook will not only be called for the actual node in a scene, but also all the throw-away instances allocated and freed by the asset API. With the help of the MAXON_CREATOR_ID unique node ID, one can establish in this firework of node reallocations which one are actually the same.

So, let us take the TagData example from your previous question and look at the data by adding the following code.

class DynamicDescriptionTagData(c4d.plugins.TagData):
    """Implements a tag hook that monitors the identity of the nodes representing the hook.
    """
    @staticmethod
    def GetNodeUuid(node: c4d.GeListNode) -> bytes:
        """Identifies a node over reallocation and scene reloading boundaries.

        The returned UUID will express what a user would consider one node. When a user creates
        a document, material, shader, object, tag, etc., it will get such UUID assigned and it
        will stay the same even over reallocation and scene reloading boundaries.
        """
        data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        return bytes(data or None)
    
    @staticmethod
    def GetIdNodeString(node: c4d.GeListNode) -> str:
        """Returns the memory location and MAXON_CREATOR_ID UUID of the node as a string.
        """
        mem: str = str(hex(id(node))).upper()
        uuid: str = str(DynamicDescriptionTagData.GetNodeUuid(node))
        return f"mem: {mem}, uuid: {uuid}"
        
    def Init(self, node: c4d.GeListNode) -> bool:
        """Called by Cinema 4D to initialize a tag instance.

        Args:
            node: The BaseTag instance representing this plugin object.
        """
        print (f"Init: {DynamicDescriptionTagData.GetIdNodeString(node)}")

        self.InitAttr(node, float, c4d.ID_OFF_X)
        self.InitAttr(node, float, c4d.ID_OFF_Y)

        node[c4d.ID_OFF_X] = 0.
        node[c4d.ID_OFF_Y] = 0.

        return True

    def Free(self, node: c4d.GeListNode) -> None:
        """Called by Cinema 4D to free a tag instance.

        Args:
            node: The BaseTag instance representing this plugin object.
        """
        print (f"Free: {DynamicDescriptionTagData.GetIdNodeString(node)}") 
    
    # ...

When we run this example with one instance of that tag in the scene and interact with the tag a bit, we will come up with something like this:

# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D530E3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D5318500, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00'
Free: mem: 0X7F95D5316640, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00'

# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D5314FC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D52E9E40, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00'
Free: mem: 0X7F95D52E30C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00'

# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D52FB3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D53096C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00'
Free: mem: 0X7F95D530A180, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00'

# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D5309F00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D52F9D00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00'
Free: mem: 0X7F95D52FD980, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00'

# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D52FD940, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D5317A80, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00'
Free: mem: 0X7F95D5313EC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00'

So, with this we can see, that:

  • Even more than in C++, the memory location of nodes (id()) is in Python completely meaningless. Nodes are reallocated all the time.
  • But one can see patterns by looking at the MAXON_CREATOR_ID UUID of nodes.
    • Only the UUID for the actual node appears more than once per Init and Free.
    • The actual node in the scene is never freed (because I never deleted the tag).
    • It is the temporary Asset API preset nodes which are freed.

What should I do?

There is no really good answer to this. Your underlying question is basically:

How can I distinguish a dummy node being passed to my node hook Init, Free, etc., from a 'real' node?

And the answer is currently: You cannot, even in C++. You can count UUIDs and exploit the patterns shown above, but that is quite unreliable. So, in the end you are unable to distinguish a dummy node being freed from a real one (at least from our current perspective).

But what you can do is detect a node being removed:

  1. By having some sort of watch node W which is watching a to be removed node R. There is no magic here, you can for example have a tag T1 which checks on each execution if the tag T2 does still exist.
  2. A bit more elegant solution is using event notifications, a non-public concept in the Cinema 4D API.

To implement (2.), you would do something like this:

    def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
                bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
        """Called when expressions are evaluated to let a tag modify a scene.

        Does nothing in this example.
        """
        if not tag.FindEventNotification(doc, tag, c4d.NOTIFY_EVENT_REMOVE):
            tag.AddEventNotification(tag, c4d.NOTIFY_EVENT_REMOVE, c4d.NOTIFY_EVENT_FLAG_NONE, c4d.BaseContainer())
        return c4d.EXECUTIONRESULT_OK
    
    def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool:
        """Called by Cinema 4D to convey events to the node.
        """
        if mid == c4d.MSG_NOTIFY_EVENT:
            print (f"{node} is being removed.")

        return True

Which would then result in:

Init: mem: 0X7FDC359CCA00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Init: mem: 0X7FDC359B6100, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01:`\x86\x01\xf0)\x00\x00'
Free: mem: 0X7FDC359B53C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01:`\x86\x01\xf0)\x00\x00'
Init: mem: 0X7FDC359C6700, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x84z\x86\x01\xfa)\x00\x00'
Free: mem: 0X7FDC359C6E00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x84z\x86\x01\xfa)\x00\x00'
Init: mem: 0X7FDC359DA640, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Free: mem: 0X7FDC359D5B00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Init: mem: 0X7FDC359D2580, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x87\xa4\x86\x01n*\x00\x00'
Free: mem: 0X7FDC359D2780, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x87\xa4\x86\x01n*\x00\x00'
Init: mem: 0X7FDC359DCBC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
<c4d.BaseTag object called DynDescription Tag/DynDescription Tag with ID 1060463 at 140583769067328> is being removed.
Free: mem: 0X7FDC359DCFC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'

But here are also a few caveats to be aware of:

  1. You will not be notified when something up in the hierarchy chain is being removed and with it your node. So, you have a null object "Null.1" and attached to it your tag "MyTag" for which you registered for the remove event. Then "Null.1" is being removed, and with it your tag. You will not be notified, and instead only when the tag itself is being removed.
  2. There also other notify events, but for NOTIFY_EVENT_FREE you will for example need at least two nodes, one watcher and one watched, as you cannot notify yourself about being freed as you can for being removed. Some of the events will also be emitted for the dummy nodes.
  3. Event notification is private, and we cannot share details on demand here. You are more or less on your own.

One Important Last Thing

Your question was unintentionally quite board without telling us what you actually intended to do. In general, wanting to distinguish these events should not be necessary when one is using the plugin hooks how they are meant to be used and respecting their limits.

There are of course exceptions to this, and sometimes the classic API can get in your way. But I would urge you to try to rethink your problem, because that is often better than trying to go head first through a wall 😉

Cheers,
Ferdinand

Full Code:

"""Implements a tag which drives the position of a host object procedurally or via tracks.
"""

import c4d
import typing


class DynamicDescriptionTagData(c4d.plugins.TagData):
    """Implements a tag which drives the position of a host object procedurally or via tracks.
    """
    # The plugin ID.
    ID_PLUGIN: int = 1060463

    # What you dubbed "user data", a dictionary holding some description data to be injected at 
    # runtime. I just changed things a bit up and include/identify things over their ID instead
    # of made-up strings, but nothing would prevent you from having both, it would just be a bit
    # more complicated to parse such data:
    #
    # INTERFACE_DATA = [
    #     {
    #         "label": "Foo", 
    #         "id": c4d.DescId(...), 
    #         "data": [
    #             {"label": "Name", "id": c4d.DESC_NAME, "value": "Bar"},
    #             ...
    #         ]
    #     },
    #     ...
    # ]
    INTERFACE_DATA: list[tuple[c4d.DescID, dict[int, typing.Any]]] = [
        (c4d.DescID(c4d.DescLevel(1000, c4d.DTYPE_REAL, 0)), 
        {
            c4d.DESC_NAME: "foo.x",
            c4d.DESC_SHORT_NAME: "foo.x",
            c4d.DESC_MIN: c4d.utils.DegToRad(-45.),
            c4d.DESC_MAX: c4d.utils.DegToRad(45.),
            c4d.DESC_MINSLIDER: c4d.utils.DegToRad(-45.),
            c4d.DESC_MAXSLIDER: c4d.utils.DegToRad(45.),
        }),
        (c4d.DescID(c4d.DescLevel(1001, c4d.DTYPE_REAL, 0)), 
        {
            c4d.DESC_NAME: "bar.y",
            c4d.DESC_SHORT_NAME: "bar.y",
            c4d.DESC_MIN: c4d.utils.DegToRad(-180.),
            c4d.DESC_MAX: c4d.utils.DegToRad(180.),
            c4d.DESC_MINSLIDER: c4d.utils.DegToRad(-180.),
            c4d.DESC_MAXSLIDER: c4d.utils.DegToRad(180.),
        }),
    ]

    @staticmethod
    def GetNodeUuid(node: c4d.GeListNode) -> bytes:
        """Identifies a node over reallocation and scene reloading boundaries.

        The returned UUID will express what a user would consider one node. When a user creates
        a document, material, shader, object, tag, etc., it will get such UUID assigned and it
        will stay the same even over reallocation and scene reloading boundaries.
        """
        data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
        return bytes(data or None)
    
    @staticmethod
    def GetIdNodeString(node: c4d.GeListNode) -> str:
        """Returns the memory location and MAXON_CREATOR_ID UUID of the node as a string.
        """
        mem: str = str(hex(id(node))).upper()
        uuid: str = str(DynamicDescriptionTagData.GetNodeUuid(node))
        return f"mem: {mem}, uuid: {uuid}"
        
    def Init(self, node: c4d.GeListNode) -> bool:
        """Called by Cinema 4D to initialize a tag instance.

        Args:
            node: The BaseTag instance representing this plugin object.
        """
        print (f"Init: {DynamicDescriptionTagData.GetIdNodeString(node)}")

        self.InitAttr(node, float, c4d.ID_OFF_X)
        self.InitAttr(node, float, c4d.ID_OFF_Y)

        node[c4d.ID_OFF_X] = 0.
        node[c4d.ID_OFF_Y] = 0.

        return True

    def Free(self, node: c4d.GeListNode) -> None:
        """Called by Cinema 4D to free a tag instance.
        """
        print (f"Free: {DynamicDescriptionTagData.GetIdNodeString(node)}")

    def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
                bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
        """Called when expressions are evaluated to let a tag modify a scene.

        Does nothing in this example.
        """
        if not tag.FindEventNotification(doc, tag, c4d.NOTIFY_EVENT_REMOVE):
            tag.AddEventNotification(tag, c4d.NOTIFY_EVENT_REMOVE, c4d.NOTIFY_EVENT_FLAG_NONE, c4d.BaseContainer())
        return c4d.EXECUTIONRESULT_OK
    
    def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool:
        """Called by Cinema 4D to convey events to the node.
        """
        if mid == c4d.MSG_NOTIFY_EVENT:
            print (f"{node} is being removed.")

        return True

    def GetDDescription(self, node: c4d.GeListNode, description: c4d.Description, 
                        flags: int) -> typing.Union[bool, tuple[bool, int]]:
        """Called by Cinema 4D when the description of a node is being evaluated to let the node
        dynamically modify its own description.
        """
        if not description.LoadDescription(node.GetType()):
            return False, flags

        singleId: c4d.DescID = description.GetSingleDescID()
        for paramId, dataDictionary in DynamicDescriptionTagData.INTERFACE_DATA:

            if (singleId and not paramId.IsPartOf(singleId)):
                return True, flags

            data: c4d.BaseContainer = description.GetParameterI(paramId)
            if data is None:
                return True, flags

            for key, value in dataDictionary.items():
                data[key] = value

        return True, flags | c4d.DESCFLAGS_DESC_LOADED


def RegisterPlugins() -> None:
    """Registers the plugins contained in this file.
    """
    if not c4d.plugins.RegisterTagPlugin(
            id=DynamicDescriptionTagData.ID_PLUGIN,
            str="DynDescription Tag",
            info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE,
            g=DynamicDescriptionTagData, description="tdyndescription",
            icon=c4d.bitmaps.InitResourceBitmap(c4d.ID_MODELING_MOVE)):
        print("Warning: Failed to register 'DynDescription' tag plugin.")


if __name__ == "__main__":
    RegisterPlugins()

Hello @ferdinand,

First of all many thanks for giving a such precise and well documented answers to issues I'm facing, that is tremendous.

Then, you confirmed what I noticed and thought about the redundancy of calling/freeing; and like you said maybe I have to approach the problem differently.

The current plugin I'm working on is use in a really specific case, that way, maybe the approach with the events notification can work. I have to test that based on what you wrote.
I was asking this because I want to reset some globals var when the TAG is removed - maybe there is a more elegant way to do, but until now I haven't found one yet.

Have a good day!
Cheers,

Christophe

Hey @mocoloco,

thank you for your answer.

I was asking this because I want to reset some globals var when the TAG is removed - maybe there is a more elegant way to do, but until now I haven't found one yet.

I am not 100% sure, but your answer somewhat implies a misconception about the lifetime of plugin hooks. They are just as volatile as nodes. When we update our code like this:

@staticmethod
    def GetIdNodeString(node: c4d.GeListNode, hook: object) -> str:
        """Returns the memory location and MAXON_CREATOR_ID UUID of the node as a string.
        """
        nodeMem: str = str(hex(id(node))).upper()
        hookMem: str = str(hex(id(hook))).upper()
        uuid: str = str(DynamicDescriptionTagData.GetNodeUuid(node))
        return f"node-mem: {nodeMem}, hook-mem: {hookMem}, node-uuid: {uuid}"
        
    def Init(self, node: c4d.GeListNode) -> bool:
        """Called by Cinema 4D to initialize a tag instance.

        Args:
            node: The BaseTag instance representing this plugin object.
        """
        print (f"Init: {DynamicDescriptionTagData.GetIdNodeString(node, self)}")

Which yield data similar to this:

# A call for the actual tag node to be added to the scene graph. 
"""Init: node-mem: 0X25D5E702F40, hook-mem: 0X25DE98A2A90, 
         node-uuid: b',\xf0]w\xccG\x17\x01\x1c|9C\xbc!\x00\x00'"""
# ...

# A call for the actual tag node to be added to the scene graph. Note that the call it NOT made to 
# the same plugin hook instance:
#
# 1: node-mem: 0X25D5E702F40, hook-mem: 0X25DE98A2A90, 
#    node-uuid: b',\xf0]w\xccG\x17\x01\x1c|9C\xbc!\x00\x00'
# 2: node-mem: 0X25D5E71A980, hook-mem: 0X25DE989A090, 
#    node-uuid: b',\xf0]w\xccG\x17\x01\x1c|9C\xbc!\x00\x00'
#
"""Init: node-mem: 0X25D5E71A980, hook-mem: 0X25DE989A090, 
         node-uuid: b',\xf0]w\xccG\x17\x01\x1c|9C\xbc!\x00\x00'"""
# ...

So, there is neither a persistent plugin hook nor a president node in the scene for your plugin. Using globals is fine as long as they are constants. But when you want to delete them, it implies that you want to treat them as variables which more often than not will lead into problems.

You could have a global dict where you store settings under node UUIDs and where you only put nodes whose UUID has appeared more than once in the NodeData.Init of yours. That will sort of work, but it would be better to attach the data to the plugin hook instance, i.e., self of MyTagData as you will side step a lot of global variable problems with that. That would mean that you have to init these values every time the node is initialized.

But that plugin hook instance approach is still non-ideal due to the volatility of hooks. The best way to handle this, is to add these values simply to the data container of the node. This way they will be serialized and deleted with the node.

Cheers,
Ferdinand

Hi @ferdinand,

Thanks a lot one more time for all the detailed examples and informations.

I finally opt to a data container of the node, mostly due to the fact that the hooks are volatile and need to be set all the time. I also ran some tests with globals without having encounter issues, but indeed you need to be careful when handling this approach.

Cheers,
Christophe