Hi
I'm looking for a way to programmatically check if a dll file is a AutoCAD plug-in, preferably getting the version too.
The only thing I can think of is using
System.Reflection.Assembly.ReflectionOnlyLoad()
to load the dll, search thru its referrences looking for Acad-specific (acdbmgd etc.) get the reference version, compare it to a list....
Is there a simpler way?
Thanks
alex
Using whether acdbmgd/acmgd.dll is referenced in a DLL or not to decide if a DLL is an AutoCAD plug-in DLL is not reliable, IMO. Actually, the term of "AutoCAD Plug-in" isn't quite clear enough to classify DLLs.
For example, we can build a few dlls, one does file/folder manupulation, another does data read/write to file/database...They do not have to have reference to acad .NET API assemblies at all. Then we can simply create a acad .NET API dll that defines coomands that execute the functions in those DLLs. In this case, can you say only the last DLL is AutoCAD Plug-in DLL, while others are not?
Maybe, you want to explain what exactly you want to achieve, then someone would have better advice/suggestion.
Norman Yuan
Norman
I'm trying to identify which dll's on my disk drive can be loaded into AutoCAD, and into which Acad version.
As such, I do not need to identify referenced dll's. Those will be pulled in by the plug-in once ir is loaded.
So I guess I have to identify those dll's which define a Acad command.
Thanks
alex
Based on your explanation, I wrote the following code to check if a DLL is an AutoCAD plug-in or not. If it is, get the AutoCAD version.
I used System.Reflection.Assembly.ReflectionOnlyLoadFrom() to load context of a DLL file from its location.
[CommandMethod("GetAutocadPluginVersion")] public static void GetAutocadPluginVersion() { string dllPath = @"C:\Temp\AutocadPlugin.dll"; string acadVersion = GetAutocadPluginVersion(dllPath); Editor editor = Application.DocumentManager.MdiActiveDocument.Editor; string message = string.IsNullOrEmpty(acadVersion) ? "This DLL is not an AutoCAD plug-in" : "This DLL is an AutoCAD plug-in that supports AutoCAD " + acadVersion; editor.WriteMessage(message); } private static string GetAutocadPluginVersion(string dllPath) { var AutoCADVersion = new Dictionary<string, string> { { "15.0", "2000" }, { "15.1", "2000i" }, { "15.6", "2002" }, { "16.0", "2004" }, { "16.1", "2005" }, { "16.2", "2006" }, { "17.0", "2007" }, { "17.1", "2008" }, { "17.2", "2009" }, { "18.0", "2010" }, { "18.1", "2011" }, { "18.2", "2012" }, { "19.0", "2013" } }; string dllName = "acdbmgd"; string acadVersion = string.Empty; Assembly assembly = System.Reflection.Assembly.ReflectionOnlyLoadFrom(dllPath); AssemblyName[] assemblyNames = assembly.GetReferencedAssemblies(); foreach (AssemblyName assemblyName in assemblyNames) { string name = assemblyName.Name; if (name.ToLower() == dllName.ToLower()) { string version = assemblyName.Version.Major + "." + assemblyName.Version.Minor; if (AutoCADVersion.ContainsKey(version)) { acadVersion = AutoCADVersion[version]; } break; } } return acadVersion; }
-Khoa
khoa:
Excelent. Works perfectly. I had some trouble initializing the Dictionary in .NET 2.0 but it's OK now.
One more question: how can we retrieve the command names defined in the plug-in?
Thanks
alex
AutoCAD command is defined by CommandMethod attribute. We use reflection MethodInfo.GetCustomAttributes() to get all custom attributes including AutoCAD CommandMethodAttribute.
Assembly.ReflectionOnlyLoadFrom() is used to get all references, so it is faster to load only context in the DLL.
Assembly.LoadFile() is used to get all contents of the DLL, so it loads all methods and their custom attributes.
I rewrote the code a little bit complicated to make it a nice custom command in AutoCAD. When user types a new command AcadPluginDetails, it will prompt to select a DLL file to check if it is an AutoCAD plug-in or not, and list all found commands in this plug-in.
[CommandMethod("AcadPluginDetails")] public static void GetAutocadPluginDetails() { Editor editor = Application.DocumentManager.MdiActiveDocument.Editor; var promptOpenFile = new PromptOpenFileOptions("Select an DLL file") { Filter = "Dynamic libraries (*.dll)|*.dll" }; PromptFileNameResult res = editor.GetFileNameForOpen(promptOpenFile); string dllPath = res.StringResult; if (string.IsNullOrEmpty(dllPath)) return; string dllName = Path.GetFileName(dllPath); string acadVersion = GetAutocadPluginVersion(dllPath); List<string> acadCommands = GetAllAutocadCommands(dllPath); string message = string.IsNullOrEmpty(acadVersion) ? "File " + dllName + " is not an AutoCAD plug-in" : "File " + dllName + " is an AutoCAD plug-in that supports AutoCAD " + acadVersion + (acadCommands.Count == 0 ? string.Empty : "\nFound commands in this plug-in: " + String.Join(", ", acadCommands.ToArray())); editor.WriteMessage(message); } private static string GetAutocadVersion(AssemblyName assemblyName) { var AutoCADVersion = new Dictionary<string, string> { { "15.0", "2000" }, { "15.1", "2000i" }, { "15.6", "2002" }, { "16.0", "2004" }, { "16.1", "2005" }, { "16.2", "2006" }, { "17.0", "2007" }, { "17.1", "2008" }, { "17.2", "2009" }, { "18.0", "2010" }, { "18.1", "2011" }, { "18.2", "2012" }, { "19.0", "2013" } }; string acadVersion = string.Empty; string version = assemblyName.Version.Major + "." + assemblyName.Version.Minor; if (AutoCADVersion.ContainsKey(version)) { acadVersion = AutoCADVersion[version]; } return acadVersion; } // Get the AutoCAD version of a given DLL file, // returns empty string if it is not an AutoCAD plug-in public static string GetAutocadPluginVersion(string dllPath) { string acadVersion = string.Empty; // Indicate the most common DLL name referencing in an AutoCAD plug-in const string dllName = "acdbmgd"; // Loads an assembly into the reflection-only context, given its path try { Assembly assembly = Assembly.ReflectionOnlyLoadFrom(dllPath); // Check if this assembly is the defined dllName AssemblyName loadedAssemblyName = assembly.GetName(); if (loadedAssemblyName.Name.ToLower() == dllName.ToLower()) { acadVersion = GetAutocadVersion(loadedAssemblyName); } else // this assembly is not acdbmgd.dll { AssemblyName[] assemblyNames = assembly.GetReferencedAssemblies(); foreach (AssemblyName assemblyName in assemblyNames) { string name = assemblyName.Name; if (name.ToLower() == dllName.ToLower()) { acadVersion = GetAutocadVersion(assemblyName); break; } } } } catch (System.Exception ex) { } return acadVersion; } // Retrieve all AutoCAD commands from a given DLL plug-in file public static List<string> GetAllAutocadCommands(string dllPath) { var commandList = new List<string>(); try { // Loads the contents of an assembly file Assembly assembly = Assembly.LoadFile(dllPath); // Get all types/classes in this assembly Type[] types = assembly.GetTypes(); foreach (Type type in types) { // Get all methods in this class MethodInfo[] methodInfos = type.GetMethods(); foreach (MethodInfo methodInfo in methodInfos) { // Get AutoCAD command's attribute object[] attributes = methodInfo.GetCustomAttributes(typeof(CommandMethodAttribute), true); if (attributes.Length > 0) { // Get AutoCAD command name string commandName = ((CommandMethodAttribute)attributes[0]).GlobalName; commandList.Add(commandName); } } } } catch (System.Exception ex) { } return commandList; }
-Khoa
Khoa
The code you posted is fine, thank you.
My problem is, I do this in an out-of-process COM.
The 1st part is OK, it identifies the Acad plug-ins and thei versions.
But, when calling GetAllAutocadCommands() I get a File Not Found exception, presumably because the CommandMethodAttribute is defined in one of the Autocad dll's, the JIT tries to pull in all the referrenced dll's and can't find or load them.
I tried using CustomAttributeData.GetCustomAttributes(Assembly) but it raises the same exception.
Is there a workaround?
Thanks
alex
I
Hi Alex,
Can you give more details about the error of "File Not Found" exception. What specific code causes that error?
CommandMethodAttribute is a class inside acmgd.dll, so make sure you reference this AutoCAD assembly in your out-of-process COM program.
Assembly.LoadFile() will raise error if it loads native DLL files (BadImageFormatException with no manifest of an unmanaged assembly), or managed assemblies built with higher .NET framework. My latest code will handle this error.
-Khoa
Hi Khoa
Yes, I do reference acmgd.dll and acdbmgd.dll
The exception is:
System.IO.FileNotFoundException: The specified module could not be found. (Exception from HRESULT: 0x8007007E)
on the line:
GetAllAutocadCommands(dllPath);
My understanding is that the JIT compiler tries to compile the method, looks for the CommandMethodAttribute type, tries to load acmgd.dll and fails (on it or one of its dependencies). I haven't been able to find what's missing (in any case acmgd references only system dll's and acdbmgd).
Thanks
alex
I guess your program may use .NET framework 2.0, but it references acdbmgd.dll and acmgd.dll in .NET higher version, such as 3.0 (A2009), 3.5 (A2010 & A2011), or 4.0 (A2012 & A2013). AutoCAD 2007 & 2008 use .NET 2.0.
Make sure your program is using equal or higher .NET version than framework of acmgd.dll. Otherwise, the reflection of lower runtime framework cannot read assemblies of higher framework, and raises errors.
-Khoa
Khoa
It uses NET 2.0 indeed, but the references are to A2008, so it should be OK.
One more detail: which I missed pasting: The FileNotFound is thrown as:
Could not load file or assembly 'acmgd, Version=17.1.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
And I noticed that the version shown in Add Reference is 17.1.51.0. Could that be it?
I even tried to put the executable in the A2008 folder, so it won't have any trouble loading dll's.
This is what happened:
******************************************************************************
System.TypeInitializationException: The type initializer for '<Module>' threw an exception. ---> <CrtImplementationDetails>.ModuleLoadException: The C++ module failed to load during process initialization.
---> System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
at AcPlPlotReactor.{ctor}(AcPlPlotReactor* )
at ?A0x860c6007.??__EgPlotReactor@@YMXXZ()
at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend)
at <CrtImplementationDetails>.LanguageSupport.InitializePerProcess(LanguageSupport* )
at <CrtImplementationDetails>.LanguageSupport._Initialize(LanguageSupport* )
at <CrtImplementationDetails>.LanguageSupport.Initialize(LanguageSupport* )
--- End of inner exception stack trace ---
at <CrtImplementationDetails>.ThrowModuleLoadException(String errorMessage, Exception innerException)
at <CrtImplementationDetails>.ThrowModuleLoadException(String , Exception )
at <CrtImplementationDetails>.LanguageSupport.Initialize(LanguageSupport* )
at .cctor()
--- End of inner exception stack trace ---
at LL.Form1.GetAutocadPluginVersion(String dllPath)
*******************************************************************************************
Does this mean anyrhing to you?
THanks
alex
Alex,
The error of "Could not load file or assembly 'acmgd, Version=17.1.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies." is the problem of reading unmanaged DLL dependencies of acmgd.dll. When your .NET program references acmgd.dll to use CommandMethodAttribute, it fails to load its native unmanaged dependencies. This is a problem of .NET reflection to load native C++ assemblies. I still don't know how to fix that. I will come to find the solution later.
-Khoa
Khoa,
>>I will come to find the solution later.
So there is still hope..
I admit that I don't understand much of this, but just a thought: What about using CustomAttributeData class and thus avoiding loading acmgd.dll at all?
Thanks
alex
Alex,
I made a new console app (without any references to AutoCAD assemblies such as acmgd.dll) and got exactly the same problem with your program. I revised my posted code to capture error events. But it is not finished to post here.
“The type initializer for '<Module>' threw an exception...” indicates that the .NET reflector fails when trying to reference unmanaged assemblies inside acmgd.dll
CustomAttributeData.GetCustomAttributes() has the same error of "Could not load file or assembly 'acmgd, Version=17.2.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies."
CustomAttributeData.GetCustomAttributes(methodInfo) is similar to methodInfo.GetCustomAttributes(true). CustomAttributeData class is more generic to accept many object types to retrieve their attributes.
The problem as I mentioned previously is .NET reflection tries to load dependent C++ assemblies during reading the AutoCAD plug-in file. It can read acmgd.dll at the AutoCAD installation folder but fails to read all its unmanaged dependencies. I still try to figure how to prevent reading those unnecessary C++ dependencies of acmgd.dll.
There is no problem if the posted code works inside AutoCAD. But outside AutoCAD as a console app, it will cause a problem.
-Khoa
Khoa
1.My Reflector 6 program suceeds in doing what I'm looking for: It finds the methods having the CommandMethod attribute, which shows that it is possible to do it.
2. From MSDN CustomAttributeData class docu:
CustomAttributeData can be used in the execution context as well as in the reflection-only context. For example, you might want to avoid loading the assembly that contains the code for a custom attribute.
2Maybe?
To make a hard task harder (for me, not you), what about the same problem for .arx files (here we know they are Acad plug-ins, we just have to find the commands).
alex
Alex,
CustomAttributeData.GetCustomAttributes() always throws exception when reading attributes. I still don't know how to prevent loading acmgd.dll and its dependencies. The .NET reflection throws errors when loading unmanaged assemblies.
.NET Reflector software and other disassembly tools can read custom attributes (of assembly, class, method) nicely without loading dependencies. I don't know how it can do that. This is something we need to learn. I found Mono.Cecil is a great library to support .NET reflection but it requires learning curve and time.
To keep you moving forward, I post the code of a sample console app that can read an AutoCAD plug-in to find its AutoCAD version, but it fails to get all AutoCAD commands. In Visual Studio, create a new simple console app (prefer .NET 4.0 to read all AutoCAD plug-in versions) without any extra references, and then copy the code to the file Program.cs.
using System; using System.Collections.Generic; using System.IO; using System.Reflection; namespace AcadReflection { class Program { static void Main(string[] args) { try { string dllPath = @"C:\Program Files\AutoCAD PID 2009\AcCalcUi.dll"; string output = GetAutocadPluginDetails(dllPath); Console.WriteLine(output); } catch (BadImageFormatException ex) { Console.WriteLine(ex.Message + "\n" + ex.StackTrace); } } #region Reflection public static string GetAutocadPluginDetails(string dllPath) { try { if (string.IsNullOrEmpty(dllPath)) return string.Empty; string dllName = Path.GetFileName(dllPath); string acadVersion = GetAutocadPluginVersion(dllPath); List<string> acadCommands = GetAllAutocadCommands(dllPath); string message = string.IsNullOrEmpty(acadVersion) ? "File " + dllName + " is not an AutoCAD plug-in" : "File " + dllName + " is an AutoCAD plug-in that supports AutoCAD " + acadVersion + (acadCommands.Count == 0 ? string.Empty : "\nFound commands in this plug-in: " + String.Join(", ", acadCommands.ToArray())); return message; } catch (BadImageFormatException ex) { throw; } } private static string GetAutocadVersion(AssemblyName assemblyName) { var AutoCADVersion = new Dictionary<string, string> { { "15.0", "2000" }, { "15.1", "2000i" }, { "15.6", "2002" }, { "16.0", "2004" }, { "16.1", "2005" }, { "16.2", "2006" }, { "17.0", "2007" }, { "17.1", "2008" }, { "17.2", "2009" }, { "18.0", "2010" }, { "18.1", "2011" }, { "18.2", "2012" }, { "19.0", "2013" } }; string acadVersion = string.Empty; string version = assemblyName.Version.Major + "." + assemblyName.Version.Minor; if (AutoCADVersion.ContainsKey(version)) { acadVersion = AutoCADVersion[version]; } return acadVersion; } // Get the AutoCAD version of a given DLL file, // returns empty string if it is not an AutoCAD plug-in public static string GetAutocadPluginVersion(string dllPath) { string acadVersion = string.Empty; // Indicate the most common DLL name referencing in an AutoCAD plug-in const string dllName = "acdbmgd"; // Loads an assembly into the reflection-only context, given its path try { Assembly assembly = Assembly.ReflectionOnlyLoadFrom(dllPath); AssemblyName loadedAssemblyName = assembly.GetName(); // Check if this assembly is the defined dllName if (loadedAssemblyName.Name.ToLower() == dllName.ToLower()) { acadVersion = GetAutocadVersion(loadedAssemblyName); } else // this assembly is not acdbmgd.dll { AssemblyName[] assemblyNames = assembly.GetReferencedAssemblies(); foreach (AssemblyName assemblyName in assemblyNames) { string name = assemblyName.Name; if (name.ToLower() == dllName.ToLower()) { acadVersion = GetAutocadVersion(assemblyName); break; } } } } catch (Exception ex) { throw; } return acadVersion; } // Retrieve all AutoCAD commands from a given DLL plug-in file public static List<string> GetAllAutocadCommands(string dllPath) { var commandList = new List<string>(); try { AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomainOnReflectionOnlyAssemblyResolve; // Loads the contents of an assembly file Assembly assembly = Assembly.ReflectionOnlyLoadFrom(dllPath); // Get all types/classes in this assembly Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException typeException) { types = typeException.Types; } foreach (Type type in types) { if (type == null) continue; // Get all methods in this class MethodInfo[] methodInfos = type.GetMethods(); foreach (MethodInfo methodInfo in methodInfos) { try { // Get AutoCAD command's attribute //object[] attributes = methodInfo.GetCustomAttributes(false); IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(methodInfo); foreach (CustomAttributeData customAttributeData in attributeDataList) { if (customAttributeData.GetType().Name == "CommandMethodAttribute") { // Get AutoCAD command name string commandName = ""; // ((CommandMethodAttribute)attributes[0]).GlobalName; commandList.Add(commandName); } } } catch (FileNotFoundException fileException) { //throw; } } } } catch (BadImageFormatException ex) { throw; } return commandList; } private static Assembly CurrentDomainOnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args) { return System.Reflection.Assembly.ReflectionOnlyLoad(args.Name); } #endregion } }
-Khoa
Khoa
>>...I found Mono.Cecil is a great library to support .NET reflection but it requires learning curve and time.
Surprisingly (or maybe not) there was almost no learning curve, due in part probably to the complete lack of documentation.
I downloaded the library and it took 15 minutes to get the results.
Here is the code, and it works:
using System; using System.Collections.Generic; using System.IO; using System.Reflection; using Mono.Cecil; using System.Text; //code not extensively tested yet //TODO add error checking //TODO fix it for Unicode. for now CustomAttribute.Blob is just raw ASCII data with some obscure formatting (additional ASCII 0 and ASCII 8 added) namespace ReflectionAcadPlugins { class Program { public static void Main() { //Creates an AssemblyDefinition from the "MyLibrary.dll" assembly AssemblyDefinition myLibrary = AssemblyFactory.GetAssembly(@"MyLibrary.dll"); //Gets all the program modules ModuleDefinitionCollection modules = myLibrary.Modules; foreach (ModuleDefinition mod in modules) { //Gets all types/classes which are declared in the module TypeDefinitionCollection types = mod.Types; foreach (TypeDefinition type in mod.Types) { if (type == null) continue; //gets all methods in this class MethodDefinitionCollection methods = type.Methods; //gets all custom attributes for this method foreach (MethodDefinition met in methods) { CustomAttributeCollection attribs = met.CustomAttributes; //checks it the attribute is what we want foreach (CustomAttribute attr in attribs) { if (attr.Constructor.ToString().Contains("CommandMethodAttribute")) { Byte[] bytes = attr.Blob; string cmd = System.Text.ASCIIEncoding.ASCII.GetString(bytes); cmd = ReplaceNonPrintableCharacters(cmd); Console.WriteLine(cmd); } } } } } } static string ReplaceNonPrintableCharacters(string s) { StringBuilder result = new StringBuilder(); for (int i = 0; i < s.Length; i++) { char c = s[i]; byte b = (byte)c; if (b >= 32) result.Append(c); } return result.ToString(); }
So your tip about Mono.Cecil was great.
Thank you
alex
Just remembered:
I still need to do the same (extracting the command names) for arx files.
I haven't a clue how to go about it.
Any ideas?
Hi Alex,
I am happy that you could make it work with Mono.Cecil library, as I found .NET framework has some limitations for reflection. Your topic is also very interesting to me to start playing with .NET Reflection.
-Khoa