Python: PluginMessage: Execute callback on plugin register and unregister event
-
Hey all,
I am currently facing the issue that I want to execute code when my cinema python plugin gets registered and unregistered by cinema or a user.
As i understand from the docs I should be able to listen to events in my
.pyp
file of my plugin by adding a function calledPluginMessage
.The event ids I am interested in are:
C4DPL_INIT Initialize the plugin, calling PluginStart.
C4DPL_END End the plugin, calling PluginEnddef PluginMessage(id: int, data: Any) -> bool: # ID's don't match up if id == c4d.C4DPL_INIT: register() return True if id == c4d.C4DPL_END: unregister() return True return False
The problem is that the incoming event ids never seems to match up.
Any idea what I am doing wrong?
-
Hey @paulgolter,
The reason why you are not seeing these is because not in all stages of the boot sequence of Cinema 4D all API systems are available. So, even in C++ you are basically in no-mans-land for both
C4DPL_INIT
andC4DPL_END
where nothing works except forstd
language features; because forC4DPL_INIT
nothing has been yet pulled up, and forC4DPL_END
everything has already been torn down. Especially when Cinema 4D is shutting down, non-careful code can lead to access violations and with that crashes. For Python the unavailability of systems is even more pronounced because Python itself only comes alive when things like registries have been pulled up. But as always, you have a harder time in Python to royally shoot yourself in the foot and with that crash Cinema 4D (but not impossible afterC4DPL_ENDPROGRAM
or evenC4DPL_ENDACTIVITY
).In general, the markers to start and stop doing things for plugin authors are
C4DPL_STARTACTIVITY
andC4DPL_ENDACTIVITY
orC4DPL_PROGRAM_STARTED
andC4DPL_ENDPROGRAM
when you want even tighter bounds where truly everything is up and running. But withC4DPL_STARTACTIVITY
you have full access to the API it is just that some calls might still fail due to certain things not having started yet. This also applies to C++.Cheers,
FerdinandLog
This is for just opening and then closing Cinema 4D. In the Python console we will usually never see anything from
C4DPL_ENDPROGRAM
onwards, as the system is there already being torn down.PluginMessage: C4DPL_STARTACTIVITY PluginMessage: C4DPL_BUILDMENU PluginMessage: C4DPL_LAYOUTCHANGED PluginMessage: C4DPL_PROGRAM_STARTED PluginMessage: C4DPL_COMMANDLINEARGS PluginMessage: C4DPL_BUILDMENU PluginMessage: Unknown Type (1062714) PluginMessage: C4DPL_LAYOUTCHANGED PluginMessage: C4DPL_ENDPROGRAM PluginMessage: C4DPL_SHUTDOWNTHREADS PluginMessage: C4DPL_ENDACTIVITY PluginMessage: Unknown Type (300002146) PluginMessage: Unknown Type (1026848) PluginMessage: Unknown Type (1026662) PluginMessage: Unknown Type (200000272)
Code
import os MESSAGE_MAP: dict[int, str] = { c4d.C4DPL_INIT_SYS: "C4DPL_INIT_SYS", c4d.C4DPL_INIT: "C4DPL_INIT", c4d.C4DPL_END: "C4DPL_END", c4d.C4DPL_MESSAGE: "C4DPL_MESSAGE", c4d.C4DPL_BUILDMENU: "C4DPL_BUILDMENU", c4d.C4DPL_STARTACTIVITY: "C4DPL_STARTACTIVITY", c4d.C4DPL_ENDACTIVITY: "C4DPL_ENDACTIVITY", c4d.C4DPL_CHANGEDSECURITYTOKEN: "C4DPL_CHANGEDSECURITYTOKEN", c4d.C4DPL_SHUTDOWNTHREADS: "C4DPL_SHUTDOWNTHREADS", c4d.C4DPL_LAYOUTCHANGED: "C4DPL_LAYOUTCHANGED", c4d.C4DPL_RELOADPYTHONPLUGINS: "C4DPL_RELOADPYTHONPLUGINS", c4d.C4DPL_COMMANDLINEARGS: "C4DPL_COMMANDLINEARGS", c4d.C4DPL_EDITIMAGE: "C4DPL_EDITIMAGE", c4d.C4DPL_ENDPROGRAM: "C4DPL_ENDPROGRAM", c4d.C4DPL_DEVICECHANGE: "C4DPL_DEVICECHANGE", c4d.C4DPL_NETWORK_CHANGE: "C4DPL_NETWORK_CHANGE", c4d.C4DPL_SYSTEM_SLEEP: "C4DPL_SYSTEM_SLEEP", c4d.C4DPL_SYSTEM_WAKE: "C4DPL_SYSTEM_WAKE", c4d.C4DPL_PROGRAM_STARTED: "C4DPL_PROGRAM_STARTED" } BOOT_LOG: str = os.path.join(os.path.dirname(__file__), "boot_log.txt") def PluginMessage(pid: int, data: any) -> bool: """Called by Cinema 4D to handle plugin messages. """ print (f"PluginMessage: {MESSAGE_MAP.get(pid, f'Unknown Type ({pid})')}") # In production you should either check for being on the main thread or use a semaphore or lock # to regulate access to #BOOT_LOG. with open(BOOT_LOG, "a") as f: f.write(f"PluginMessage: {MESSAGE_MAP.get(pid, f'Unknown Type ({pid})')}\n") return False
-
Hey @ferdinand,
thanks for the detailed background info. Would have never found that out on my own!
Ok currently I am using the regular python way to call my register() function, which register my message and object plugins:
if __name__ == "__main__": register()
If i try to replace that with:
def PluginMessage(id: int, data: Any) -> bool: if id == c4d.C4DPL_STARTACTIVITY: register() return True
I get the error:
OSError:cannot find pyp file - plugin registration failed
So i guess just like you said, some calls are still failing. Then I will stick to registering the plugin with the simple
if __name__ == "__main__"
way and unregistering works fine with the C4DPL_ENDACTIVITY event. -
Hey @paulgolter,
The way how plugins are registered in C++ does not translate to Python. Just do it as you did before and as shown in all Python code examples: Register them in the execution context guard (
if __name__ ...
) of a pyp file.C4DPL_STARTACTIVITY
is too late to register plugins,C4DPL_INIT
would be the point to do that. But that does no translate to Python as Python itself is a plugin, so it cannot run before plugins have been loaded. When you register anObjectData
plugin in Python, you get for example a preallocated plugin slot assigned from the Python API. Which is also why users can also only have 50 Python plugins perNodeData
type installed (object, tag, shader, etc.) as the Python API just reserves this many slots per node type. So, the 51thRegisterObjectPlugin
etc. call will fail on a user machine.It is pretty obvious that you are trying to do something specific. Maybe you could try to explain on a more abstract level what you are trying to achieve, and I could then give options there, instead of just telling you what all does not work for technical reasons
Cheers,
Ferdinand -
Hey @ferdinand
I see!
Maybe you could try to explain on a more abstract level what you are trying to achieve
In short, I am building a python project, that depends on being able to communicate with a webserver. It sends out things when certain events happen in cinema and also receives signals via websockets to execeute certain actions. The communication with the server is all async and runs inside of a thread to not block the dccs ui.
Most dccs that enable / disable plugins, have some sort of functions or event that you can hook in when this happens. That way I can for instance, when my plugins is loaded, establish a connection with the webserver and when it is unloaded making sure that I gracefully shutdown the connection, the async loop as well as the thread.
-
Hey @paulgolter,
I still do not 100% understand why this entails you having to poke around in the boot sequence of Cinema 4D. You should just register your plugin normally (there is no alternative to that). But at least I understand now where we are going
- Python plugins cannot be disabled in Cinema 4D. You can reload them, but not disable them. You also cannot unregister a plugin. The only way to do this is to reboot Cinema 4D and then bail on registration, for example based on a web server response or something like that.
- In general, you might run into problems with
async/await
in our Python interpreter, it is not a keyword/pattern that is used often in the context of the Cinema 4D API. You must keep in mind that there is the Cinema 4D main loop, the scene execution state machine. So, other than in a vanilla Python VM, the Cinema 4D Python VM does not run uninterrupted, but only when Cinema 4D calls/executes the plugin (Script Manager scripts of course run in one go, but these are besides the point here). It is hard to make here an absolute statement, but in general I would stay away fromasync
. Python's threading is also off-limits and you must use c4d.threading, themultiprocessing
module is not supported.
In which context do you want to use
async
? To run a webserver with something likefastapi
orsanic
? I would not recommend running such server under Cinema 4D, this might cause a lot of problems. Instead you should run your webserver with a vanilla Python where you dot not have Cinema 4D lurking over your shoulder and limiting how you can uses threads and processes. You then can communicate with the Cinema 4D Python VM via sockets (or a similar technique for inter-process/app communication). You might want to have a look at these two threads:- Server & Client pattern to have two Python VMs communicate : This example uses this to run a non-native GUI, but you can use that for everything that does not run well under Cinema. The problematic bit is that you must be able to serialize all your data, as you have to send it over a socket.
- Downloading stuff in Cinema 4D in a non-blocking manner: For that you do not need
async
, just some threading and usually some spider in the net plugin hook which handles the downloading of content (exposed to other plugins), a common choice as shown in the example is aMessageData
plugin.
Cheers,
Ferdinand -
Hey @ferdinand
I still do not 100% understand why this entails you having to poke around in the boot sequence of Cinema 4D. You should just register your plugin normally (there is no alternative to that)
I think it's mainly because I didn't know what the proper way of doing it was. Reading the docs I thought I needed to use
c4d.C4DPL_INIT
orc4d.C4DPL_STARTACTIVITY
to register my plugin stuff / execute my own startup code. I know there are the c4d plugin examples, but maybe it would be nice to add the "proper" way of registering a plugin to the docs.Python plugins cannot be disabled in Cinema 4D. You can reload them, but not disable them. You also cannot unregister a plugin.
Aha also some good information that I was not aware of!
And to the server:
I am not running a server inside of cinema. It is running on some other machine in the internet. I am only connecting to it via ahttpx.AsyncClient
client. For this reason and some other technical reasons the client needs to run in a separate thread, but all of that works fine and we are even using thec4d.C4DThread
for this.But you answered my question of this thread already, thanks