Test plugin written on C# without reloading 3d MAX


#21

The ephere MaxDotNet Utility plugin no longer exists. All of the values that were exposed in the Utility plugin are stored in the .config file I outlined.

-Eric


#22

Well everything going good. But if i open the plugin then close it and trying reopen it gives the error “can`t access to disposed object”. Could anyone give advice how to avoid it.


#23

Did anyone try to recompile this plugin unloader by Alex Budovski for versions above 2010? maxplugins.de
I experience same exact compile unresolved errors LNK2019 as somebody noted here

Any chance to get it working for newer versions?
Or maybe there’re another methods available to develop c++ maxscript exposed functions without reloads?

I also found another decade old thread in russian where is seems like possible.


#24

for all versions after MAX 8 (as I remember) you can’t unload DLL … it was not safe for most DLLs, and probably MAX developers decided to completely eliminate this possibility.

so, there is no way to unload c++ SDK DLLs

but depending on how you wrote your plugin you can override the old one (I hope). You can do it for example with the most of .NET DLLs


#25

Thank you, Denis.
My cpp skills are below novice so I won’t be able to override anything in next few years (optimistically) :slight_smile:
Reloading c# dll isn’t a problem. I was hoping that everybody just forgot about that possibility in c++ sdk, but no luck.
Need to ask Santa for a brand new ssd


#26

Upto and including 2010 it is possible to unload a dll (except mxs dlx’s). It’s a convoluted task and not for the faint hearted requiring some custom tools for checking whether max is running and what version is running from visual studio. Then the tool (i use python with a custom extension) uses max’s com interface (peeking/hacking the resource entry as it goes) to save the current file, reset max, unload the dll (requires it’s own custom dlx!). you can now copy over the new dll, and reload it finally reload the saved file. Simples :slight_smile: Since 2010 version they no longer export/make public the key function that lets you unload the dll. So I do all my primary dev in 2010.

here’s some of the python post build script i use (WinProcess is a custom c extension to python)

#**************************************************************************************************

#file:     UpdateMaxPluginEx.py
#purpose:  manage visual studio post build event for max plugins 
#started:  
#last ed:  
#usage:    UpdateMaxPluginEx.py [-p/--filepath <pluginname>] [-v/--version <int>] [-x/--x64] [-r/--restartmax] [-q/--quitmax]

#python ..\..\..\scripts\UpdateMaxPluginEx.py -p $(TargetPath)   -v 17 -q --x64
#**************************************************************************************************

import os;
import shutil;
import time;
import win32api;
#our custom extension
import WinProcess;
import win32com.client;
import sys;
import getopt;
import os.path;

from MaxOLE import *;

#**********************************************************************
# initialize stuff

supportedversions = [11,12,13,14,15,16,17];
max_functions = ["OLECommand","OLESaveMaxFile","OLELoadLastSavedFile","OLEResetMax","OLEQuitMax","OLEUnloadPlugin","OLELoadPlugin"];
mvStrToInt = {"3ds Max 2009":11,"3ds Max 2010":12,"3ds Max 2011":13,"3ds Max 2012":14,"3ds Max 2013":15,"3ds Max 2014":16,"3ds Max 2015":17};
mvIntToStr = {11:"2009",12:"2010",13:"2011",14:"2012",15:"2013",16:"2014",17:"2015"};
sleeptime = 3;
args = sys.argv;
version = 0;
archi = "x86";
restartMax = False;
quitMax = False;

MaxAppName = "3dsmax.exe";
MaxApp = "MAX.Application";
MaxAppKeys = CollectMaxAppKeys(MaxApp);
usageStr = "UpdateMaxPluginEx.py [-p/--filepath <pluginname>]  [-v/--version <int>] [-x/--x64] [-r/--restartmax] [-q/--quitmax]";

#**********************************************************************
# Parse arguments, shouldn't really fuck up as it has very controlled usage
# but hey ho I may forget, I mean I will forget ! 

try:
    options, remainder = getopt.gnu_getopt(sys.argv[1:], 'p:v:xrq', ['filepath=', 'version','x64','restartmax','quitmax']);
except getopt.GetoptError, err:
        # print help information and exit:
        print str(err); 
        print usageStr;
        sys.exit(2);

# parse the args

for opt, arg in options:
    if opt in ('-p', '--filepath'): 
        sourcefile = arg;
    elif opt in ('-v', '--version'):
        version = int(arg);
    elif opt in ('-x', '--x64'):
        archi = "x64";    
    elif opt in ('-r', '--restartmax'):
        restartMax = True;
    elif opt in ('-q', '--quitmax'):
        quitMax = True;

if not version in supportedversions:
    print "Script Error: Unsupported version, exiting UpdateMaxPluginEx.py"
    sys.exit();

print ("**** 3ds Max "+ mvIntToStr[version] + " " + archi + " ****");

pfiles = os.environ["ProgramFiles(x86)"];
if archi == "x64":
    pfiles = os.environ["ProgramFiles"];

# get the short plugin name generate the destination file

pluginName = os.path.basename(sourcefile);
destfile = pfiles + "\\Autodesk\\3ds Max " + mvIntToStr[version] + "\\plugins\\" + pluginName;
FullMaxAppName = pfiles + "\\Autodesk\\3ds Max " + mvIntToStr[version] + "\\3dsmax.exe"

print ("Running..." + os.path.basename(args[0]));
print ("Destination File... " + destfile);

#get current max that is running see if it's ours

print FullMaxAppName;
max_is_running = False;
winprocess_res = WinProcess.IsRunningEx(FullMaxAppName);
if winprocess_res == 1:
    max_is_running = True;
    #print max_app_path;
    #max_app_path_list = max_app_path.split(os.sep);
    #progfiles = max_app_path_list[0] + os.sep + max_app_path_list[1];

    #max_app_archi = "x86";
    #if progfiles == os.environ["ProgramFiles"]:
     #   max_app_archi = "x64";

    #max_app_version = mvStrToInt[max_app_path_list[3]];
   # print max_app_version;
   # print max_app_archi;
    #if max_app_version == version and max_app_archi == archi:
     #   max_is_running = True;

# our max is not running do the copy

if not max_is_running:
    print (FullMaxAppName  + " Is Not running on this System");
    print "Copying file...\n" +  sourcefile + "\nto...\n" + destfile + "\n"; 
    shutil.copy(sourcefile, destfile);
    if restartMax:
        print "starting 3dsmax.exe";
        if getOLESupport(MaxAppKeys, version):
            win32com.client.Dispatch(MaxApp + '.' + str(version));
        else:
            print "Unable to start Max as Specified version Not OLE supported";
    print "UpdateMaxPlugin.py complete";         
    sys.exit();

# ok max is running make sure it's the current OLE

#if setCurrentMaxAppVersion(MaxAppKeys, version):
#    setCurrentMaxAppArchitexture(MaxAppKeys, archi);
#else:
#   print "Script Error: Unable to Interact with Max as Specified version Not OLE supported";
#    sys.exit();

keyindex = getOLESupport(MaxAppKeys, version)
if keyindex != -1:
    MaxApp += ".";
    MaxApp += str(version); 
    setOLELocalServer32(MaxAppKeys, keyindex, MaxApp);
    print "Dispatching to... " + MaxApp;
else:
    print "Script Error: Unable to Interact with Max as Specified version Not OLE supported";
    sys.exit();    

# ok should be able to talk to max now

conn = win32com.client.Dispatch(MaxApp);
for funcs in max_functions: # add our OLE functions
    conn._FlagAsMethod(funcs);

# save the current file

print "Saving Max file";
res = conn.OLESaveMaxFile();

if quitMax: # quit max option used for dlx plugins as they can't be unloaded and versions after 2010
    
    print "Exiting Max";
    try:
        res = conn.OLEQuitMax();
    except:
        res;
    time.sleep(sleeptime);    #give max time to shut down
    print "Copying file...\n" +  sourcefile + "\nto...\n" + destfile + "\n"; 
    shutil.copy(sourcefile, destfile);
    if restartMax:   # only restart if we actually have quit max
        conn = win32com.client.Dispatch(MaxApp);
        #conn.OLELoadLastSavedFile(); #
        
else: # we risk it for a swisskit 
    
    print "Reseting Max";
    try:
        res = conn.OLEResetMax();
    except:
        res;
    print "Unloading Plugin"; 
    conn.OLEUnloadPlugin(pluginName);
    print "Copying file...\n" +  sourcefile + "\nto...\n" + destfile + "\n"; 
    shutil.copy(sourcefile,destfile);
    print "Loading Plugin";
    conn.OLELoadPlugin(pluginName);
    print "Loading Saved File"; 
    conn.OLELoadLastSavedFile();

print "UpdateMaxPluginEx.py complete";

i made a gif to show the benefit to my work flow…


[SDK] How do you test your plugins? Restart 3ds every time?
#27

wow! looks very cool… maybe too cool for me

it’s very far from i do. my business is c++ dlx and dlm :slight_smile:


#28

I’ve managed to enable unloading in versions after 2010…It’s a bit of a hack…

struct dlldesc_struct
{
	int*	pad[5];	
	bool	loaded;
};

#if MAX_VERSION_MAJOR > 13 

		HINSTANCE hinst = plgDesc->GetHandle();
		FreeLibrary(hinst); // use the window function that max would use

// and hack the memory so max knows it's unloaded

		DllDesc *plgDesc_temp = const_cast<DllDesc*>(plgDesc); // cast away the const
		dlldesc_struct** dll_desc_strut = reinterpret_cast<dlldesc_struct**>(plgDesc_temp);
		(*dll_desc_strut)->loaded = false;

#else
// this would be the 2010 way of doing it (when the functions were public

#29

If I unload and then try to reload the dll without recompile it shows me the dialog saying that such ClassID is already registered. Max crashes after a while.
%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5

If I unload and then try to reload recompiled dll it shows me the dialog saying that dll compile dates are mismatching and then after a while max crashes.
%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5

that’s the code I use to load and unload


LoadDllsFromDir (getdir #maxroot + "plugins\\") "MaterialID*.dlm"  -- loading
unloaddll() -- unloading
 

dlx

struct dlldesc_struct
{
	int*	pad[5];
	bool	loaded;
};

def_visible_primitive(unloaddll, "unloaddll");
Value* unloaddll_cf(Value** arg_list, int count)
{
	DllDir* dd = (GetCOREInterface7())->GetDllDirectory();
	MaxSDK::Util::Path dllpath;
	dllpath.SetPath(_T("hard-coded-path-to-my-dlm-file"));

	if (!dd)
	{
		mprintf(_T("DllDir is NULL\n"));
		return &false_value;
	}

	int index = dd->FindDllDescFromDllPath(dllpath);
		
	mprintf(_T("Dll index is %d\n"), index);

	if ( index == -1 ) return &false_value;	

	const DllDesc* desc = &dd->GetDllDescription(index);

	mprintf(_T("Unloading mod: %s\n"), desc->Description());
	

	HINSTANCE hinst = desc->GetHandle();
	FreeLibrary(hinst);

	DllDesc *plgDesc_temp = const_cast<DllDesc*>(desc); // cast away the const
	dlldesc_struct** dll_desc_strut = reinterpret_cast<dlldesc_struct**>(plgDesc_temp);

	(*dll_desc_strut)->loaded = false;


	return &true_value;
}

I’ve tried to set CanAutoDefer to false but it didn’t change anything.
What else should I do to make it work in 2014 ?
I hope that I can do better than just first two steps :rofl:


#30

oh thats not nearly enough :grinning: !

struct dlldesc_struct
{
	int*	pad[5];
	bool	loaded;
};


def_visible_primitive(UnloadPlugin, "UnloadPlugin");

// UnloadPlugin "pgModifiers.dlm"

Value* UnloadPlugin_cf(Value **arg_list, int count)
{
	check_arg_count(UnloadPlugin, 1, count);
	const TCHAR* name = arg_list[0]->to_string();

	DllDir *dd = MAXScript_interface->GetDllDirectory();
	DllDesc *plgDesc = GetDescFromPlugname(name, MAXScript_interface);

	
	dllerror = 0;
	if(!plgDesc)
	{
		dllerror = kdll_notfound;
		mprintf(_T("kdll_notfound\n"));
		return &false_value;
	}


#if MAX_VERSION_MAJOR > 13 
	if(!plgDesc->IsLoaded())
#else
	if(!plgDesc->loaded)
#endif
	{
		dllerror = kdll_unloaded;
		mprintf(_T("kdll_unloaded\n"));
		return &false_value;
	}

	int na = 0;
	for(int i = 0; i < plgDesc->NumberOfClasses(); ++i)
	{
		ClassDesc* classdesc = (*plgDesc)[i];
		if(!classdesc)
			continue;
		ClassEntry* classentry = dd->ClassDir().FindClassEntry(classdesc->SuperClassID(),classdesc->ClassID());
		if(classentry)
			na += classentry->UseCount();
	}
	if(na == 0)
	{
		int noc = plgDesc->NumberOfClasses();
		int unloadCount = 0;
		for(int i = 0 ;i < noc; ++i)
		{
			ClassDesc* classdesc = (*plgDesc)[i];
			if (!classdesc) 
				continue; 
			unloadCount += (MAXScript_interface->DeleteClass(classdesc));
		}

		if(unloadCount != noc)
		{
			dllerror = kdll_cannot_unload;
			mprintf(_T("kdll_cannot_unload (unloadCount == noc)\n"));
			return &false_value;
		}

#if MAX_VERSION_MAJOR > 13 

		HINSTANCE hinst = plgDesc->GetHandle();
		FreeLibrary(hinst);

		//DllDesc *plgDesc_temp = const_cast<DllDesc*>(plgDesc);
		dlldesc_struct** dll_desc_strut = reinterpret_cast<dlldesc_struct**>(plgDesc);
		(*dll_desc_strut)->loaded = false;

#else
		TCHAR out[MAX_PATH];

		GetModuleFileName(plgDesc->handle, out, MAX_PATH-1);
		TCHAR path[MAX_PATH];
		BMMSplitFilename(out, path, NULL, NULL);

	#ifdef _UNICODE
		plgDesc->directory.printf(L"%s",path);
	#else
		plgDesc->directory.printf("%s",path);
	#endif
		plgDesc->tDescription = plgDesc->Description();
		plgDesc->Free();
		plgDesc->loaded = FALSE;

#endif

	}
	else
	{
		dllerror = kdll_cannot_unload;
		mprintf(_T("kdll_cannot_unload (na != 0)\n"));
		return &false_value;
	}
	return &true_value;
}

#31

Thanks, Klvnk.
I had to make it the way it was intially otherwise it refuses to compile.


DllDesc *plgDesc_temp = const_cast<DllDesc*>(plgDesc);
dlldesc_struct** dll_desc_strut = reinterpret_cast<dlldesc_struct**>(plgDesc_temp);
(*dll_desc_strut)->loaded = false;
 

If I try to reload without recompile it works just fine now. No window shows up.
But after recompile this Deferred Dll error still keeps showing up… :confused:


#32

is it actually a deferred dll ? if it is try it as non deferred


#33

something in the back of my mind says that dll error might be something to do with admin rights


#34

I run max with the admin rights so it is unlikely to be the issue.

__declspec(dllexport) ULONG CanAutoDefer()
{
return 1;
}
__declspec(dllexport) ULONG CanAutoDefer()
{
return 0;
}

tried bothof these CanAutoDefer
doesn’t affect the error dialog

for some reason if I run this code after the unloadPlugin

(
fn pad val w:10 pc:"_" =
(
	if w > 0 then
	( 
		if isKindOf pc string and pc != "" then -- pad with char
		(
			(dotNetObject "system.string" (val as string)).padRight w pc
		)
		else
		(
			(dotNetObject "system.string" (val as string)).padRight w
		)
			
	)
	else
	if w < 0 then ( (dotNetObject "system.string" (val as string)).padLeft -w )
	else ( val as string )
)

dd = (dotNetClass "Autodesk.Max.GlobalInterface").Instance.dlldir.instance

for i=0 to dd.count-1 do
(
	plug = dd.item i 
	format "%: %loaded:%defferable:%\n" (pad i w:-3) (pad plug.FileName w:34 pc:"_") (pad plug.IsLoaded w:8) (try (plug.IsDeferrable) catch ("[ error ]"))

)

)

it clearly shows that max don’t really delete the class when we call UnloadPlugin

306: materialid_modifier.dlm___________loaded:false___defferable:[ error ]
307: partgrav.dlm______________________loaded:false___defferable:true
308: proteus.dlm_______________________loaded:false___defferable:true

upd…
nope, class is actually deleted from OSM modifiers list

next thing to try is to substitute timestamp of new dll with the old one.
upd2
it didn’t worked out either