Loading 3ds Max .NET Assemblies


The plug-in loader has been created specifically for .NET plug-ins which makes it safer for 3ds Max to load and unload .NET and mixed-mode 3ds Max plug-ins. Assemblies (DLLs) which contain .NET or C++/CLI plug-ins should now be placed in the bin/assemblies subfolder of the 3ds Max installation, instead of the plugins folder. Note: Placing managed mixed-mode assemblies in stdplugs and not in bin/assemblies, is risky and can crash the application.

Managed Maxscript Plugins

Starting with release 2013, the plugin loader can now load managed maxscript plugin extensions (*.dlx). This is used, for instance, with the maxsdk sample project mxsdotnet.dlx which contains managed code. These Maxscript assemblies require not one but two entry methods. The extra method it needs to implement is: public static void AssemblyLoad(). This method is called first by the managed loader, even before AssemblyMain() described below.

Using the Assembly Loader

The assembly loader loads plug-ins from the bin/assemblies folder in the 3ds Max program folder. Each plug-in is given a chance to execute some initialization code. The assembly loading process happens in four phases (Or five for maxscript plugins):

  1. All assemblies in the bin/assemblies directory are loaded into memory. This simply means calling Assembly.LoadFromContext()on the Assembly. Note: Starting with release 2013 no sub directories are searched.
  2. For managed maxscript assemblies the loader executes AssemblyLoad(). This is necessary to initialize static global variables, which is crucial for proper maxscript initialization. These static globals must be initialized before the main maxscript entry method is called. This is because in native code, calling LoadLibrary will be sufficient to construct all global and static global variables. But in .NET, calling Assembly.Load will NOT construct these static global variables. This is because a .NET assembly will only construct native static global variables the first time that managed code is executed. Thus it is not necessary to put any actual code in the AssemblyLoad() method. In fact for maxscript plugins, this function must be empty. Thus any attempt to use the maxscript types defined in the assembly will fail, the plugin will fail to get properly initialized, and the assembly will be ignored.
  3. Next, the loader executes the first entry point method that it finds in each assembly. An entry point method must exist on a public type and must match one of the following two signatures:
    • public static void AssemblyMain()
    • public static void AssemblyMain(AssemblyLoader.Loader loader)

    This second form is passed the current executing loader object. This form is useful if an assembly needs to examine types from other loaded classes during the next loading phase. For managed maxscript assemblies, this functions should execute normal maxscript initialization methods like any other native maxscript plugin.

  4. The loader examines every publicly exported type from each loaded assembly and passes each type in turn to its TypeLoaded event handlers.
  5. When completed the loader executes the first clean up method that it finds in each assembly. As with AssemblyMain, this method must exist on a public type and must match one of the following two signatures for the Loader to recognize it:
    • public static void AssemblyInitializationCleanup()
    • public static void AssemblyInitializationCleanup(AssemblyLoader.Loader loader)

    Note that it is not necessary to remove the TypeLoaded event handlers. These are cleared automatically at the end of the clean-up phase.

When the application shuts down, the loader runs over all loaded assemblies and calls a shutdown method. For the Loader to recognize the method, the method must exist in a public type, and it must match one of the following two signatures:

These shutdown methods can be used to release any native resources and to delete any remaining native objects. Each hook indicated above is entirely optional. The plug-in designer may choose to use none, any, or all of these hooks: the AssemblyMain method, TypeLoaded event handlers, the AssemblyInitializationCleanup method, or the AssemblyShutdown method.

It is important that .NET assemblies avoid native global objects: objects allocated during DLL load, and released only during DLL shutdown. This includes every type other than basic primitives or arrays of primitives. Any actual object (class or struct) would have a constructor or destructor, even if it is generated by the compiler, and executing the constructor at load time or the destructor during application termination may cause the execution of managed code outside the CLR's lifetime. This can cause the application to crash, or to enter a deadlock. Instead of global objects, plug-ins should use dynamically allocated objects explicitly constructed during AssemblyMain(), and explicitly destructed during AssemblyShutdown().

Converting native plugins to managed assemblies

For those who want to convert a native Global Utility Plugin (*.gup) to a managed assembly, please note: The managed plugin loader is agnostic about what code is executed in it's AssemblyMain() methods. A GUP plugin type that inherits from class GUP, and compiled into a managed mixed-mode assembly will not get initlialized. In fact the managed loader will have no idea it even exists and will simply ignore it. Therefore it makes no sense to compile classes that inherit from class GUP into a managed assembly. The plugin loader also is agnostic about any other type of plugin class that inherit from standard plugin classes in the maxsdk. Meaning it will ignore those as well.