SOLVED Using CodeEditor_Open()

Hello,

I'm experimenting with an object that accepts Python code. Therefore, I also need an "Open Editor" button to open the Python code in the code editor dialog.

I have found this old thread here:
https://plugincafe.maxon.net/topic/10352/13847_how-to-pass-python-code-to-c4ds-editor-window/2?_=1618561938314

However, the code shown there does not work anymore.

SCRIPTMODE enum

bc.SetInt32(CODEEDITOR_SETMODE, SCRIPTMODE_PYTHON);

I guess this must nowadays be:

bc.SetInt32(CODEEDITOR_SETMODE, (Int32)SCRIPTMODE::PYTHON);

GePythonGIL

case CODEEDITOR_SETSTRING:
    {
      GePythonGIL gil;
      data->SetString(PARAMETER_PYTHONSCRIPT, msg.GetString(CODEEDITOR_SETSTRING));
  
      res = true;
      break;
    }

First of all, there is no GePythonGIL anymore, so the code does not compile. Is maxon::py::CPythonGil the correct replacement?

Second, what is it needed for? It's declared, but never used in this scope.

PythonLibrary

It seems this was in lib_py.h, but the SDK docs mention in the "Changes in R23" chapter that it's deprecated now. So, what should be used instead?

pylib.CheckSyntax()

I can also not find anything about CheckSyntax() in the current SDK docs.

Callback function signature

Even with an empty callback function that doesn't do anything, the code does not compile.

No matching function for call to 'CodeEditor_Open' it says.

The API defines the callback like this:

Bool CodeEditor_Open(BaseList2D* obj, const maxon::Delegate<GeData(BaseList2D* obj, const BaseContainer& msg)>& callback, const BaseContainer& bc = BaseContainer());

So it seems that has also changed since the thread I mentioned.

. . .

Long story short, I would really appreciate an up-to-date example of how to use CodeEditor_Open() and everything that has to do with it.

Thanks in advance!

Greetings,
Frank

Hi @fwilleke80, I'm currently preparing a full example.
However, I'm wondering do you need to run a whole script at each evaluation, or act more like the current Python Generator?
If it's the second case do you want to deal with Classic API types (aka passing/receiving a BaseList2D from/to Python)? Or stick to Maxon API only (it's possible but will make some stuff a bit harder since you need to manage both)

Cheers,
Maxime.

It is supposed to work a little like the Python Generator, yes.

But it's being evaluated by my terrain generation system. So I guess, yes, I need to run a whole script on each evaluation. In that regard, it's probably most comparable to the Python Field Object or the Python Effector.

These are 2 different things.
Do you want to execute a specific function (so having 1 CodeBlock that could be used for different areas, like in the Python Generator, where the Message Function is called independently of the Main function), or execute all the code specified in your Code Block?

Cheers,
Maxime.

Ah, ok. I didn't even know that the Python Generator supports a Message() function 😄

Yes, that's exactly what I would like to have! The user should be able to implement some functions, and get provided with some variables to work with (like in the Python Effector).

Hi sorry for the delay, but I'm going to provide you the full example of a Python Generator Object using only the python.framework by Monday 🙂

At least I can answer about the purpose of GePythonGIL.
Python have a GIL (for more information see What Is the Python Global Interpreter Lock (GIL)?) to prevent synchronization issue.

So when a GePythonGIL is created if you look at the source code in lib_py.h it will allocate the Python GIL and at the end of the scope, since GePythonGIL is allocated on the stack, will be automatically deallocated and therefore the GIL will be released.

And you are correct, maxon::py::CPythonGil is the replacement of the GePythonGil.

Cheers,
Maxime.

Hi Franck, sorry for the long delay. So I will answers you with a step by step for implementing your own basic Python Object Generator 🙂
cube.gif

First, the resource file will consist of a code field and a button.
In opythonexample.h

#ifndef OPYTHON_EXAMPLE_H__
#define OPYTHON_EXAMPLE_H__

enum
{
	OPYTHON_EXAMPLE_CODE = 1000,
	OPYTHON_EXAMPLE_OPENEDITOR = 1001
};

#endif // OPYTHON_EXAMPLE_H__

In opythonexample.res

CONTAINER Opythonexample
{
	NAME Opythonexample;
	INCLUDE Obase;

	GROUP ID_OBJECTPROPERTIES
	{
		SCALE_V;

		BUTTON OPYTHON_EXAMPLE_OPENEDITOR { }

		STRING OPYTHON_EXAMPLE_CODE
		{
		  CUSTOMGUI MULTISTRING;
		  PYTHON;
		  SCALE_V;
		}
	}    
}

And the opythonexample.str

STRINGTABLE Opythonexample
{
	Opythonexample		"C++ SDK - Python Object";

	OPYTHON_EXAMPLE_CODE			"Code";
	OPYTHON_EXAMPLE_OPENEDITOR 		"Open Python Editor";
}

So let's create a basic ObjectData with just a special handling for the code parameter (done in GetDParameter) in order to be aware if the code changed.

#include "c4d_objectdata.h"
#include "c4d_general.h"
#include "opythonexample.h"

#include "lib_description.h"


#define PLUGIN_ID_PYTHON_OBJECT 1057128

class PythonExampleObject : public ObjectData
{
	INSTANCEOF(PythonExampleObject, ObjectData)

private:
	Bool codeChanged = false; // Store if the code changed and therefore the Python code be re-compiled

public:
	virtual Bool Init(GeListNode* node)
	{
		// We call SetParameter in this case, so SetDParameter is called and codeChanged == true
		String code = "import c4d\nimport maxon\n\ndef GetVirtualObjects(op):\n    return c4d.BaseObject(c4d.Ocube)\n\nprint(f'This will be called for each _scope.Execute(), value of {doc}')";
		node->SetParameter(DescID(OPYTHON_EXAMPLE_CODE), code, DESCFLAGS_SET::NONE);

		return true;
	};

	maxon::Result<void> CreateInitAndExecuteScope(BaseObject* obj)
	{
		iferr_scope;
		if (obj == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Passed BaseObject is nullptr"_s);

		BaseContainer* data = ((BaseObject*)obj)->GetDataInstance();
		if (data == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Unable to retrieve the BaseContianer"_s);

		String code = data->GetString(OPYTHON_EXAMPLE_CODE);

		_scope = _vm.CreateScope() iferr_return;
		_scope.Init("SDK Python Object"_s, code, maxon::ERRORHANDLING::PRINT) iferr_return;

		// Add Doc reference to the all scope
		MAXON_SCOPE
		{
			maxon::py::CPythonGil lock(_pyLibMaxonAPI);
		  
			BaseDocument* doc = obj->GetDocument();
			if (doc == nullptr)
			{
				_scope.Add("doc"_s, maxon::Data(_pyLibMaxonAPI.CPy_None())) iferr_return;
				return maxon::OK;
			}

			maxon::specialtype::BaseDocument* docSpecial = reinterpret_cast<maxon::specialtype::BaseDocument*>(doc);
			_scope.Add("doc"_s, maxon::Data(docSpecial)) iferr_return;
		}

		_scope.Execute() iferr_return;

		codeChanged = false;

		return maxon::OK;
	}

	virtual BaseObject* GetVirtualObjects(BaseObject* op, HierarchyHelp* hh)
	{
        // Return nothing for the moment
		return nullptr;
	};

	virtual Bool SetDParameter(GeListNode* node, const DescID& id, const GeData& t_data, DESCFLAGS_SET& flags)
	{
		if (node == nullptr)
			return false;

		switch (id[0].id)
		{
			// This parameter will not be stored in the BaseContainer
			case OPYTHON_EXAMPLE_CODE:
			{
				codeChanged = true;
				break;
			}
		}

		return SUPER::SetDParameter(node, id, t_data, flags);
	}

	static NodeData* Alloc()
	{
		return NewObjClear(PythonExampleObject);
	}
};

Bool RegisterPythonObject()
{
	return RegisterObjectPlugin(PLUGIN_ID_PYTHON_OBJECT, "C++ SDK - Python Object"_s, OBJECT_GENERATOR | OBJECT_USECACHECOLOR , PythonExampleObject::Alloc, "Opythonexample"_s, nullptr, 0);
}

Then next step is to add Python functionality but first of all there is few information that you should know for the next step which will consist to create a Python Scope.

So A Python Scope (VirtualMachineScopeInterface) and more especially the CPythonScope will help you to manage your Python code.
CPythonScopeRef::Init will compile (create Python op code) of the passed code and create globals and locals dict.
CPythonScopeRef::Execute will actually run the compiled opcode, it's important to execute the scope at least once, since python it's a pretty dynamic language and this wil lcreate Python object for everything (e.g. class, function).
When the CPythonScopeRef is destructed, the Python Garbage collector is called to free any leftover (using GC_Collect, so it may not be immediate but this is another topic). And the Python Exception state is reseted.

Then in order to do it properly, you need to deal with the GIL. The idea of the GIl is rather simple, the Python VM store some information like like the exception state(did an exception occured during the last Python Execution?).
Now let's think about 2 threads executing some code to the Python VM, since exception state is a boolean stored at the VM level, you need some way to syncrhonize that and if script A is failing you still want script B to run.
So the GIL(Global Interpreter Lock) is just a way to say to the Python VM, I'm script A and I'm executing so don't let any action that may change your internal state until I told you I don't care anymore.
This is a a rough summary but you get teh idea, if you want more information see What Is the Python Global Interpreter Lock (GIL)?

Lets do some code, to create an helper method to help us in thecreation of a valid scope each time we need.
We are going to store the created scope in a member called _scope.
This way we are not forced to re-compile for each evaluation the python code only when the code changed, and use already existing data.

#include "maxon/vm.h"
#include "maxon/cpython.h"
#include "maxon/cpython3_raw.h"
#include "maxon/cpython_c4d.h"

MAXON_DEPENDENCY_WEAK("maxon/vm.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython3_raw.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython_c4d.h");


class PythonExampleObject : public ObjectData
{

private:
    const maxon::VirtualMachineRef& _vm = MAXON_CPYTHON3VM();  // reference to the Python 3 VM
    const maxon::py::CPythonLibraryRef _pyLibMaxonAPI = maxon::Cast<maxon::py::CPythonLibraryRef>(_vm.GetLibraryRef()); // reference to the Python 3 Library
    maxon::VirtualMachineScopeRef _scope; // Will store the previous compiled scope.

public:
	maxon::Result<void> CreateInitAndExecuteScope(BaseObject* obj)
	{
		iferr_scope;
		if (obj == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Passed BaseObject is nullptr"_s);

		BaseContainer* data = ((BaseObject*)obj)->GetDataInstance();
		if (data == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Unable to retrieve the BaseContianer"_s);

		String code = data->GetString(OPYTHON_EXAMPLE_CODE);

        // Create a Python Scope, this will override existing one.
        // Since its a Maxon Reference deallocation will be handled automatically by the Maxon API)
		_scope = _vm.CreateScope() iferr_return;

        // Initialize the scope with the given code and express how we want to deal with error (in our case print them)
		_scope.Init("SDK Python Object"_s, code, maxon::ERRORHANDLING::PRINT) iferr_return;

		// Add Doc reference to the all scope
		MAXON_SCOPE
		{
			// Since we will use the Python Library to create Python object. The GIL must be held.
			maxon::py::CPythonGil lock(_pyLibMaxonAPI);
		  
			BaseDocument* doc = obj->GetDocument();
			if (doc == nullptr)
			{
				_scope.Add("doc"_s, maxon::Data(_pyLibMaxonAPI.CPy_None())) iferr_return;
				return maxon::OK;
			}

			// Only maxon.Data can be added to a Maxon Scope (This is the tricky part for Python and Python.framework, since in Python you mostly deal with Classic API type)
			// So a maxon::specialtype::BaseDocument is a Maxon Data type that know how to convert a BaseDocument to a Python BaseObject.
			// In this case the create Python object will not own the BaseDocument, a maxon::specialtype::BaseDocumentMove* would own the passed BaseDocument.
			maxon::specialtype::BaseDocument* docSpecial = reinterpret_cast<maxon::specialtype::BaseDocument*>(doc);
			_scope.Add("doc"_s, maxon::Data(docSpecial)) iferr_return;
		}

		// We finally execute our scope, so Python evaluate it and build everything
		_scope.Execute() iferr_return;

		codeChanged = false;

		return maxon::OK;
	}
}

Then let's implement the most important part, aka Calling the Python GetVirtualObjects from the C++ GetVirtualObjects

	virtual BaseObject* GetVirtualObjects(BaseObject* op, HierarchyHelp* hh)
	{
        // Since we will use the Python Library to create Python object. The GIL must be held.
		maxon::py::CPythonGil lock(_pyLibMaxonAPI);

		iferr_scope_handler
		{
			return nullptr;
		};

        // If the code changed and a GetVirtualObject pass is called, (re)build the scope
		if (codeChanged)
		{
			CreateInitAndExecuteScope(op) iferr_return;
		}

        // A BaseArray of MaxonData that will serve as argument to be passed to the Python function.
		maxon::BaseArray<maxon::Data*> args;

        // Same things that we saw CreateInitAndExecuteScope with the BaseDocument but this time with a BaseObject 
		maxon::specialtype::BaseObject* opSpecial = reinterpret_cast<maxon::specialtype::BaseObject*>(op);
		maxon::Data pyData = maxon::Data(opSpecial);
		args.Append(&pyData) iferr_return;
		
        // HelperStack is not meaningfull to us, but its necesarry to provide it as it will act as a DataHolder during the Python execution.
		maxon::BlockArray<maxon::Data> helperStack;

        // And we call the Python function named "GetVirtualObjects" (it's important that the scope is executed before, so this function object execit in the globals dict of the Python scope)
        // Then the 3rd arguments is important since it define the expected type to be returned.
        // In this case its not anymore a maxon::specialtype::BaseObject but a maxon::specialtype::BaseObjectMove.
        // We use a BaseObjectMove because in the Python call, we return a BaseObject, that is owned by the Python object.
        // But this python object will be destructured after the PrivateInvoke call, so therefor the BaseObject. So we need to move the BaseObject to have still it alive.
		auto* res = _scope.PrivateInvoke("GetVirtualObjects"_s, helperStack, maxon::GetDataType<maxon::specialtype::BaseObjectMove*>(), &args.ToBlock()) iferr_return;

		if (res == nullptr)
			return nullptr;
		
		maxon::specialtype::BaseObjectMove* pyRes = res->Get<maxon::specialtype::BaseObjectMove*>().GetValue();
		if (pyRes == nullptr)
			return nullptr;

        // Retrieve the BaseObject returned by the Python code
		BaseObject* finalObj = reinterpret_cast<BaseObject*>(pyRes);

		return finalObj;
	};

And finally in order to properly react to the "Open Python Editor" button.
We will need to call CodeEditor_Open when the user press the button.

class PythonExampleObject : public ObjectData
{
public:
	
	virtual Bool Message(GeListNode* node, Int32 type, void* data)
	{
		switch (type)
		{
			case MSG_DESCRIPTION_COMMAND:
			{
				DescriptionCommand* dc = (DescriptionCommand*)data;

				switch (dc->_descId[0].id)
				{

					// The editor button was pressed
					case OPYTHON_EXAMPLE_OPENEDITOR:
					{
						CodeEditor_Open(static_cast<BaseObject*>(node), PythonObjectExpressionCallBack);
						break;
					}
				}
				return true;
				break;
			}

			default: 
				break;
		}

		return SUPER::Message(node, type, data);
	}
}

Then we need to implement the callback (PythonObjectExpressionCallBack) that needs to be passed to CodeEditor_Open.
Unfortually due to a bug in the python.framework, when you compile a string it's not possible to retrieve easily (in less than 200 lines) the columns of where the error occured, so for this part I used the old python library.

static GeData PythonObjectExpressionCallBack(BaseList2D* node, const BaseContainer& msg)
{
	GeData res;
	BaseObject* obj = (BaseObject*)node;
	if (obj == nullptr)
		return res;

	BaseContainer* data = ((BaseObject*)obj)->GetDataInstance();
	if (data == nullptr)
		return res;

	switch (msg.GetId())
	{
		// Return the string to set inside the Script Expression editor.
		case CODEEDITOR_GETSTRING:
		{
			BaseContainer r;
			r.SetString(CODEEDITOR_SETSTRING, data->GetString(OPYTHON_EXAMPLE_CODE));
			r.SetInt32(CODEEDITOR_SETMODE, (Int32)SCRIPTMODE::PYTHON);
			res = r;
			break;
		}

		// Update the code string from the Script Expression editor.
		case CODEEDITOR_SETSTRING:
		{
			// We call SetParameter so SetDParameter is called and therefor codechanged==true
			node->SetParameter(DescLevel(OPYTHON_EXAMPLE_CODE), msg.GetString(CODEEDITOR_SETSTRING), DESCFLAGS_SET::NONE);
			EventAdd();
			res = true;
			break;
		}

		// Compile the code string to Python OPcode (Only check for Syntax error)
		// This is called before CODEEDITOR_EXECUTE
		case CODEEDITOR_COMPILE:
		{
			GePythonGIL pygil;
			PythonLibrary pylib;

			BaseContainer bc;
			String err_string;
			Int32 err_line = 0, err_row = 0;

			if (pylib.CheckSyntax(data->GetString(OPYTHON_EXAMPLE_CODE), &err_row, &err_line, &err_string))
			{
				bc.SetInt32(CODEEDITOR_GETERROR_RES, true);
				bc.SetInt32(CODEEDITOR_GETERROR_LINE, -1);
				bc.SetInt32(CODEEDITOR_GETERROR_POS, -1);
			}
			else
			{
				bc.SetInt32(CODEEDITOR_GETERROR_RES, false);
				bc.SetString(CODEEDITOR_GETERROR_STRING, "SyntaxError: " + err_string);
				bc.SetInt32(CODEEDITOR_GETERROR_LINE, err_line);
				bc.SetInt32(CODEEDITOR_GETERROR_POS, err_row);
			}

			res = bc;
			break;
		}

		// Execute the code string (usually triggering an update of the active document).
        // CODEEDITOR_COMPILE is always called before
		case CODEEDITOR_EXECUTE:
		{
			BaseContainer r;

			EventAdd();

			r.SetInt32(CODEEDITOR_GETERROR_RES, true);
			r.SetString(CODEEDITOR_GETERROR_STRING, String());
			r.SetInt32(CODEEDITOR_GETERROR_LINE, -1);
			r.SetInt32(CODEEDITOR_GETERROR_POS, -1);

			res = r;
			break;
		}

        // This is used to syncronize UI, it should return the DescID of the code parameter
		case CODEEDITOR_GETID:
		{
			return GeData(CUSTOMDATATYPE_DESCID, DescID(DescLevel(OPYTHON_EXAMPLE_CODE)));
			break;
		}

	}
	return res;
}

And thats it 😄

So here is the full code if you are interested.

#include "c4d_objectdata.h"
#include "c4d_general.h"
#include "opythonexample.h"

#include "lib_description.h"
#include "c4d_libs/lib_py.h"

#include "maxon/vm.h"
#include "maxon/cpython.h"
#include "maxon/cpython3_raw.h"
#include "maxon/cpython_c4d.h"

MAXON_DEPENDENCY_WEAK("maxon/vm.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython3_raw.h");
MAXON_DEPENDENCY_WEAK("maxon/cpython_c4d.h");


#define PLUGIN_ID_PYTHON_OBJECT 1057128


static GeData PythonObjectExpressionCallBack(BaseList2D* node, const BaseContainer& msg)
{
	GeData res;
	BaseObject* obj = (BaseObject*)node;
	if (obj == nullptr)
		return res;

	BaseContainer* data = ((BaseObject*)obj)->GetDataInstance();
	if (data == nullptr)
		return res;

	switch (msg.GetId())
	{
		// Return the string to set inside the Script Expression editor.
		case CODEEDITOR_GETSTRING:
		{
			BaseContainer r;
			r.SetString(CODEEDITOR_SETSTRING, data->GetString(OPYTHON_EXAMPLE_CODE));
			r.SetInt32(CODEEDITOR_SETMODE, (Int32)SCRIPTMODE::PYTHON);
			res = r;
			break;
		}

		// Update the code string from the Script Expression editor.
		case CODEEDITOR_SETSTRING:
		{
			// We call SetParameter so SetDParameter is called and therefor codechanged==true
			node->SetParameter(DescLevel(OPYTHON_EXAMPLE_CODE), msg.GetString(CODEEDITOR_SETSTRING), DESCFLAGS_SET::NONE);
			EventAdd();
			res = true;
			break;
		}

		// Compile the code string to Python OPcode (Only check for Syntax error)
		// This is called before CODEEDITOR_EXECUTE
		case CODEEDITOR_COMPILE:
		{
			GePythonGIL pygil;
			PythonLibrary pylib;

			BaseContainer bc;
			String err_string;
			Int32 err_line = 0, err_row = 0;

			if (pylib.CheckSyntax(data->GetString(OPYTHON_EXAMPLE_CODE), &err_row, &err_line, &err_string))
			{
				bc.SetInt32(CODEEDITOR_GETERROR_RES, true);
				bc.SetInt32(CODEEDITOR_GETERROR_LINE, -1);
				bc.SetInt32(CODEEDITOR_GETERROR_POS, -1);
			}
			else
			{
				bc.SetInt32(CODEEDITOR_GETERROR_RES, false);
				bc.SetString(CODEEDITOR_GETERROR_STRING, "SyntaxError: " + err_string);
				bc.SetInt32(CODEEDITOR_GETERROR_LINE, err_line);
				bc.SetInt32(CODEEDITOR_GETERROR_POS, err_row);
			}

			res = bc;
			break;
		}

		// Execute the code string (usually triggering an update of the active document).
		case CODEEDITOR_EXECUTE:
		{
			BaseContainer r;

			EventAdd();

			r.SetInt32(CODEEDITOR_GETERROR_RES, true);
			r.SetString(CODEEDITOR_GETERROR_STRING, String());
			r.SetInt32(CODEEDITOR_GETERROR_LINE, -1);
			r.SetInt32(CODEEDITOR_GETERROR_POS, -1);

			res = r;
			break;
		}

		case CODEEDITOR_GETID:
		{
			return GeData(CUSTOMDATATYPE_DESCID, DescID(DescLevel(OPYTHON_EXAMPLE_CODE)));
			break;
		}

	}
	return res;
}


class PythonExampleObject : public ObjectData
{
	INSTANCEOF(PythonExampleObject, ObjectData)

private:
	const maxon::VirtualMachineRef& _vm = MAXON_CPYTHON3VM();
	const maxon::py::CPythonLibraryRef _pyLibMaxonAPI = maxon::Cast<maxon::py::CPythonLibraryRef>(_vm.GetLibraryRef());
	maxon::VirtualMachineScopeRef _scope;

	Bool codeChanged = false;

public:
	virtual Bool Init(GeListNode* node)
	{
		iferr_scope_handler
		{
			return false;
		};

		// We call SetParameter in this case, so SetDParameter is called and codeChanged == true
		String code = "import c4d\nimport maxon\n\ndef GetVirtualObjects(op):\n    return c4d.BaseObject(c4d.Ocube)\n\nprint(f'This will be called for each _scope.Execute(), value of {doc}')";
		node->SetParameter(DescID(OPYTHON_EXAMPLE_CODE), code, DESCFLAGS_SET::NONE);

		return true;
	};

	maxon::Result<void> CreateInitAndExecuteScope(BaseObject* obj)
	{
		iferr_scope;
		if (obj == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Passed BaseObject is nullptr"_s);

		BaseContainer* data = ((BaseObject*)obj)->GetDataInstance();
		if (data == nullptr)
			return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION, "Unable to retrieve the BaseContianer"_s);

		String code = data->GetString(OPYTHON_EXAMPLE_CODE);

		_scope = _vm.CreateScope() iferr_return;
		_scope.Init("SDK Python Object"_s, code, maxon::ERRORHANDLING::PRINT) iferr_return;

		// Add Doc reference to the all scope
		MAXON_SCOPE
		{
			maxon::py::CPythonGil lock(_pyLibMaxonAPI);
		  
			BaseDocument* doc = obj->GetDocument();
			if (doc == nullptr)
			{
				_scope.Add("doc"_s, maxon::Data(_pyLibMaxonAPI.CPy_None())) iferr_return;
				return maxon::OK;
			}

			maxon::specialtype::BaseDocument* docSpecial = reinterpret_cast<maxon::specialtype::BaseDocument*>(doc);
			_scope.Add("doc"_s, maxon::Data(docSpecial)) iferr_return;
		}

		_scope.Execute() iferr_return;

		codeChanged = false;

		return maxon::OK;
	}

	virtual BaseObject* GetVirtualObjects(BaseObject* op, HierarchyHelp* hh)
	{
		maxon::py::CPythonGil lock(_pyLibMaxonAPI);

		iferr_scope_handler
		{
			return nullptr;
		};

		if (codeChanged)
		{
			CreateInitAndExecuteScope(op) iferr_return;
		}

		maxon::BaseArray<maxon::Data*> args;

		maxon::specialtype::BaseObject* opSpecial = reinterpret_cast<maxon::specialtype::BaseObject*>(op);
		maxon::Data pyData = maxon::Data(opSpecial);
		args.Append(&pyData) iferr_return;
		
		maxon::BlockArray<maxon::Data> helperStack;
		auto* res = _scope.PrivateInvoke("GetVirtualObjects"_s, helperStack, maxon::GetDataType<maxon::specialtype::BaseObjectMove*>(), &args.ToBlock()) iferr_return;

		if (res == nullptr)
			return nullptr;
		
		maxon::specialtype::BaseObjectMove* pyRes = res->Get<maxon::specialtype::BaseObjectMove*>().GetValue();
		if (pyRes == nullptr)
			return nullptr;

		BaseObject* finalObj = reinterpret_cast<BaseObject*>(pyRes);

		return finalObj;
	};

	virtual Bool Message(GeListNode* node, Int32 type, void* data)
	{
		switch (type)
		{
			case MSG_DESCRIPTION_COMMAND:
			{
				DescriptionCommand* dc = (DescriptionCommand*)data;

				switch (dc->_descId[0].id)
				{

					// The editor button was pressed
					case OPYTHON_EXAMPLE_OPENEDITOR:
					{
						CodeEditor_Open(static_cast<BaseObject*>(node), PythonObjectExpressionCallBack);
						break;
					}
				}
				return true;
				break;
			}

			default: 
				break;
		}

		return SUPER::Message(node, type, data);
	}

	virtual Bool SetDParameter(GeListNode* node, const DescID& id, const GeData& t_data, DESCFLAGS_SET& flags)
	{
		if (node == nullptr)
			return false;

		switch (id[0].id)
		{
			// This parameter will not be stored in the BaseContainer
			case OPYTHON_EXAMPLE_CODE:
			{
				codeChanged = true;
				break;
			}
		}

		return SUPER::SetDParameter(node, id, t_data, flags);
	}

	static NodeData* Alloc()
	{
		return NewObjClear(PythonExampleObject);
	}
};

Bool RegisterPythonObject()
{
	return RegisterObjectPlugin(PLUGIN_ID_PYTHON_OBJECT, "C++ SDK - Python Object"_s, OBJECT_GENERATOR | OBJECT_USECACHECOLOR , PythonExampleObject::Alloc, "Opythonexample"_s, nullptr, 0);
}

If you have any question, feel free to ask.
Cheers,
Maxime.

Wow, I'm overwhelmed. Thank you so much!
I'll dissect this and put it to good use. Thanks again for your extensive work on this!

Cheers,
Frank

Hello @fwilleke80,

without any further questions or replies, we will consider this topic as solved by Monday and flag it accordingly.

Thank you for your understanding,
Ferdinand

Sorry, thanks, yes, it is solved 🙂

Hello @fwilleke80,

thank you for the quick reply.

Cheers,
Ferdinand

Just checking this out again...

What is the use of MAXON_DEPENDENCY_WEAK() compared to a simple #include? The docs don't say much about when to choose which.

Cheers,
Frank

Your example, by the way, works fine! Thank you again for that! I'm currently trying to extend it to all my needs 🙂

Therefore, here's another question:

What if I don't want to have a BaseObject as a result, but something different, e.g. a BaseArray with values? The user would implement several different functions, some of which return a simple Int32 value, others get a BaseArray<Float> and should work on the array values, possibly changing them.

maxon::BaseArray<Float> valueArray;

auto* res = _scope.PrivateInvoke("MyFunctionThatFillsAnArrayOfFloats"_s, helperStack, maxon::GetDataType<maxon::specialtype::SomethingThatWorksWithAnArrayOfFloats*>(), &args.ToBlock()) iferr_return;

if (res == nullptr)
	return maxon::NullptrError(MAXON_SOURCE_LOCATION, "PrivateInvoke() result is NULL!"_s);

maxon::specialtype::SomethingThatWorksWithAnArrayOfFloats* pyRes = res->Get<maxon::specialtype::SomethingThatWorksWithAnArrayOfFloats*>().GetValue();
if (pyRes == nullptr)
	return maxon::NullptrError(MAXON_SOURCE_LOCATION, "SomethingThatWorksWithAnArrayOfFloats result is NULL!"_s);

Is there a maxon::specialtypethat allows me to pass a maxon::BaseArray<Float>, or a custom struct {} or even a maxon::BaseArray<MyStruct>?

Thanks again! 🙂

Cheers,
Frank

P.S.: The Python Field does something similar, it implements the Sample() function and passes all these neat custom data structures to it.

Hi @fwilleke80 sorry for the late reply.
To pass data from C++ to Python the best way is to pass Maxon::Data. A maxon::specialtype::BaseObject is a Maxon::Data (see Getting Started: Data).

An int32 being a Maxon::Data can be converted as shown with the maxon::specialtype::BaseObject but instead use maxon::Int.

For BaseArray, this is a bit tricky since BaseArray is not a Maxon::Data, but python does have some special handling to convert a BaseArray to a Python BaseArray object. While it's possible I would really recommend using Python list with CPythonLibraryInterface::CPyList_New and not directly BaseArray.

maxon::BaseArray<Float> valueArray;
valueArray.Append(10.0) iferr_return;

maxon::BaseArray<maxon::Data*> args;
// BaseArray are a bit special since they are not maxon DataType, to have the BaseArray type just had [A] to the type.
iferr (maxon::DataType resultType = maxon::DataType::Get(maxon::Id("float64[A]")))
	return nullptr;

// Use the Python special handling to create a Python object holding the BaseArray.
// Otherwise you will need to build a Python list with CPythonLibraryInterface::CPyList_New.
// OWNERSHIP::NORMAL/CALLEE/CALLER_BUT_COPY will do a copy of the Array while CALLER will do a std::move on the BaseArray.
auto expected = _pyLibMaxonAPI.CPyData_Type();
auto pyResult = _pyLibMaxonAPI.CPyObject_FromGeneric(resultType, (maxon::Generic*)&valueArray, maxon::OWNERSHIP::NORMAL, &expected);
if (pyResult == nullptr)
	return nullptr;

// Add the data hold by the python object to the list of argument.
maxon::Data pyData = maxon::Data(pyResult.Get());
args.Append(&pyData) iferr_return;
	
maxon::BlockArray<maxon::Data> helperStack;
maxon::Data* res = _scope.PrivateInvoke("MyFunctionThatFillsAnArrayOfFloats"_s, helperStack, resultType, &args.ToBlock()) iferr_return;
if (res == nullptr)
	return nullptr;

// Convert back the data to the original BaseArray
if (resultType == res->GetType())
{
	maxon::BaseArray<Float>* arr = (maxon::BaseArray<Float>*)res->PrivateGetPtr();
	if (arr == nullptr)
		return nullptr;

	ApplicationOutput("@", valueArray[0]);
	valueArray.Flush();
	valueArray.CopyFrom(*arr) iferr_return;

	ApplicationOutput("@", valueArray[0]);
}

Is there a maxon::specialtypethat allows me to pass a maxon::BaseArray<Float>, or a custom > struct {} or even a maxon::BaseArray<MyStruct>?

Yes, it's possible, but MyStruct should be registered as a MAXON_DATATYPE. However, this is a bit tricky since you also need to provide a Python Object which maps this struct (while this is possible I can't promise anything regarding documentation about how to provide such Python Object since it will require some time to do).
For the moment the easiest way for simple datatype would be to serialize your struct as a maxon::Tuple or even translate your struct as a Python Dict with CPythonLibraryInterface::CPyDict_New and add such dict to a Python List.

Hope it helps,
Cheers,
Maxime.

Hi Maxime,

Thank you! That’s a lot to take in 🙂

Cheers,
Frank