Expose script functions from .NET plug-in?

Expose script functions from .NET plug-in?

vidar.rinne
Contributor Contributor
2,474 Views
9 Replies
Message 1 of 10

Expose script functions from .NET plug-in?

vidar.rinne
Contributor
Contributor

Hi all,

I've been searching for a while about if it is possible to expose script functions from a .NET plug-in?

 

def_visible_primitive macro works well for C++ and it is accessible through MaxScript and Python, but I've no idea if this is possible in a C# .NET plug-in.

 

There is a CuiActionCommandAdapter but as far as I understand it is only available from UI.

 

Any ideas?

0 Likes
Accepted solutions (2)
2,475 Views
9 Replies
Replies (9)
Message 2 of 10

Serejah
Advocate
Advocate
Accepted solution

C# SDK definitely needs more examples than what's now available in the reference...

Yes, what you want is possible:

using Autodesk.Max;

namespace MyNamespace
{
    public class MyFuncs
    {
        public static IValue MyFunc( IValue val, int count )
        {
            GlobalInterface.Instance.TheListener.EditStream.Puts( ">>> MyFunc\n" );
            return GlobalInterface.Instance.TrueValue;
        }

        static public bool RegisterMaxscriptFunction( string name )
        {
            return GlobalInterface.Instance.Primitive.Create( name, MyFunc, (short)(primitive_flag.LazyPrimitive | primitive_flag.DebuggerSafe) ) != null;
        }

        public MyFuncs()
        {
            RegisterMaxscriptFunction( "MyFunc" );
        }

        public static void AssemblyLoad()
        {
        }
        public static void AssemblyMain()
        {
        }
    }
}

 

Compile this code to .dlx and put it in /maxroot/bin/assemblies folder

Then, whenever you create an instance of the class functions defined in class will be registered

x = dotNetObject "MyNamespace.MyFuncs"

 or you can make this class static and call RegisterMaxscriptFunction whenever you need to.

 

I hope someone more experienced than I am could tell a little bit more about how is it supposed to work.

Message 3 of 10

vidar.rinne
Contributor
Contributor

Thank you!

 

I got the function registered in Max and managed to print out a message!

 

On to another problem, an IValue gets sent into that function but I can't get the value out. In this case a simple string. ToString, but I guess this is somewhat related to C++ where there can be multiple values in there (as we get a count). 

 

There is some kind of collection in there that can be traversed with next and prev, but again it is unclear what it is underneath. The debugger does not really tell me anything.

 

There's also a native pointer in there.

0 Likes
Message 4 of 10

vidar.rinne
Contributor
Contributor

And yes, it would be great with some additional documentation for Max .NET, just a few entries.

 

I tried adding an argument count check, but apparently I'm not allowed to throw ArgCountError as it is not based on System.Exception. Throwing System.Exception in a script function results in unknown error in the listener. In C++ we were also using UserThrownError to get some additional information out.

 

ValueMetaClass was also helpful in the C++ plug-in, I do not seem to get any proper information from this tag in C# .NET. Just by assigning the tag to some variable there's crashes, sometimes at startup, sometimes when debugging.

 

I will do some more investigation, but at this moment it feels like accessing functionality through dotnetobject (and simply skip script functions) is the best option.

0 Likes
Message 5 of 10

Serejah
Advocate
Advocate
Accepted solution

Maybe we need to use specific Autodesk.Max.Wrappers.CustomMarshaler* class to access this args list, but I couldn't make it work whatever I tried.

 

Message 6 of 10

Swordslayer
Advisor
Advisor

@Serejah  schrieb:

Maybe we need to use specific Autodesk.Max.Wrappers.CustomMarshaler* class to access this args list, but I couldn't make it work whatever I tried.

 


The IValue is a standin for Value**  note that it's a pointer to a pointer so to get the real IValue, you have to get the pointer at the address of the pointer first. Here's an example:

 

 

(
	local compilerParams = dotNetObject "System.CodeDom.Compiler.CompilerParameters" #(
		getDir #maxRoot + "Autodesk.Max.dll",
		getDir #maxRoot + "\bin\assemblies\Autodesk.Max.Wrappers.dll")
	compilerParams.GenerateInMemory = on
	compilerParams.CompilerOptions = "/unsafe"

	local compilerResults = (dotNetObject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource compilerParams #(
		"using System;
		using Autodesk.Max;
		using System.Runtime.InteropServices;
		using Wrappers = Autodesk.Max.Wrappers;

		class MyFuncs
		{
			private static readonly IGlobal Global = Autodesk.Max.GlobalInterface.Instance;

			public static unsafe IValue MyFunc(IValue argsPtr, int argCount)
			{
				if (argCount == 1)
				{
					var valPtr = Marshal.ReadIntPtr(argsPtr.NativePointer, 0);
					var val = (IValue)Wrappers.CustomMarshalerValue.GetInstance(string.Empty).MarshalNativeToManaged(valPtr);
					var pt = val.EvaluateValue.ToPoint3();

					Global.TheListener.EditStream.Puts(String.Format(\">>> [{0}, {1}, {2}]\\n\", pt.X, pt.Y, pt.Z));
					return Global.TrueValue;
				}
				else return Global.FalseValue;
			}

			public MyFuncs()
			{
				Global.Primitive.Create(\"MyFunc\", MyFunc, (short)(primitive_flag.LazyPrimitive | primitive_flag.DebuggerSafe));
			}
		}"
	)

	for err = 0 to compilerResults.errors.count - 1 do print (compilerResults.errors.item[err].ToString())

	compilerResults.CompiledAssembly.CreateInstance "MyFuncs"
)

 

 

Now you can call let's say MyFunc x_axis and get the matching printout.

0 Likes
Message 7 of 10

Serejah
Advocate
Advocate

Thank you for the explanation.

I couldn't make it work in 2020 no matter how I compile it (via maxscript or using visual studio and then place .dll in bin folder).

It still feels like we shouldn't register primitives after max start. All my attempts to call the function ended up with freezes, crashes and errors like this one:

EXCEPTION_ACCESS_VIOLATION The thread tried to read from or write to a virtual address for which it does not have the appropriate access.

 

It could be a very convenient method to pass maxscript values back and forth to/from c# side. But not this time I guess 🙂

 

0 Likes
Message 8 of 10

Swordslayer
Advisor
Advisor

Interesting, it 'works' the same for me on 2020 and 2021, ie it gives the expected output until the gc kicks in, then I'm getting garbage collection warnings. Another way it breaks is that it ceases to be a primitive - this is what apropos "myfunc" outputs before it gets broken: MyFunc (const Primitive): MyFunc() and this is after: MyFunc (const <internal>): (Global:MyFunc Global:x_axis).  This is when the last argument it was called with before breaking was x_axis.

 

But I'm not running it as a startup file, I guess by the time that you get to calling the function after max completes the scene setup, it will already be broken. Did you try running it from the script editor after max is already up and running? For good measure, you can put a few test function calls right after the snippet, too.

0 Likes
Message 9 of 10

Serejah
Advocate
Advocate

@Swordslayer  написал (-а):
Did you try running it from the script editor after max is already up and running?

Yesterday it didn't work at all, but today it magically runs and works, but only once. And then I experience all the same troubles as before: garbage collection message, freeze and crash on any attempt to call the function again.

I did try to 'cache' the result of Global.Pimitive.Create in a static class field, but it doesn't seem to help at all.

 

class MyFuncs
		{
			private static readonly IGlobal Global = Autodesk.Max.GlobalInterface.Instance;
			private static Autodesk.Max.Primitive _primitive;
			...
			public MyFuncs()
			{
				_primitive = Global.Primitive.Create(\"MyFunc\", MyFunc, (short)(primitive_flag.LazyPrimitive | primitive_flag.DebuggerSafe));
			}
			...

 

 

0 Likes
Message 10 of 10

Swordslayer
Advisor
Advisor

Yeah, no idea. For some reason, I don't get crashes/freezes, only the gc error with recommended restart. The function then breaks (turns to the <internal> applied value instead of Primitive function), but funnily enough if I use globalVars.remove "MyFunc" to get rid of the duplicate fn error on reevaluation and reevaluate the snippet, it will keep working after the next gc() and I couldn't find a way to break it anymore (but with each new gc() call, I'm getting the repeated garbace collection dialogs, see attached).

 

garbage.png

Ok, now I got a random crash as well...

0 Likes