2 // mono-shlib-cop.cs: Check unmanaged dependencies
5 // mcs mono-shlib-cop.cs -r:Mono.Posix -r:Mono.GetOptions
8 // Jonathan Pryor (jonpryor@vt.edu)
10 // (C) 2005 Jonathan Pryor
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36 // mono-shlib-cop is designed to inspect an assembly and report about
37 // potentially erroneous practices. In particular, this includes:
38 // - DllImporting a .so which may be a symlink (which typically requires the
39 // -devel packages on Linux distros, thus bloating installation and
41 // - DllImporting a symbol which doesn't exist in the target library
45 // - Each assembly needs to be loaded into an AppDomain so that we can
46 // adjust the ApplicationBase path (which will allow us to more reliably
47 // load assemblies which depend upon assemblies in the same directory).
48 // We can share AppDomains (1/directory), but we (alas) can't use a
49 // single AppDomain for the entire app.
51 // - Create AppDomain with ApplicationBase path set to directory assembly
53 // - Create an AssemblyChecker instance within the AppDomain
54 // - Check an assembly with AssemblyChecker; store results in AssemblyCheckInfo.
59 // - Make -r work correctly (-r:Mono.Posix should read Mono.Posix from the
60 // GAC and inspect it.)
65 using System.Collections;
66 using System.Diagnostics;
68 using System.Reflection;
69 using System.Runtime.InteropServices;
72 using Mono.GetOptions;
75 [assembly: AssemblyTitle ("mono-shlib-cop")]
76 [assembly: AssemblyCopyright ("(C) 2005 Jonathan Pryor")]
77 [assembly: AssemblyDescription ("Looks up shared library dependencies of managed code")]
78 [assembly: Mono.Author ("Jonathan Pryor")]
79 [assembly: Mono.UsageComplement ("[ASSEMBLY]+ [-r:ASSEMBLY_REF]+")]
80 [assembly: Mono.ReportBugsTo ("jonpryor@vt.edu")]
82 namespace Mono.Unmanaged.Check {
84 sealed class MessageInfo {
87 public string Message;
89 public MessageInfo (string type, string member, string message)
96 public override bool Equals (object value)
98 MessageInfo other = value as MessageInfo;
102 return Type == other.Type && Member == other.Member &&
103 Message == other.Message;
106 public override int GetHashCode ()
108 return Type.GetHashCode () ^ Member.GetHashCode () ^
109 Message.GetHashCode ();
113 sealed class MessageCollection : MarshalByRefObject {
114 private ArrayList InnerList = new ArrayList ();
116 public MessageCollection ()
120 public int Add (MessageInfo value)
122 if (!InnerList.Contains (value))
123 return InnerList.Add (value);
124 return InnerList.IndexOf (value);
127 public void AddRange (MessageInfo[] value)
129 foreach (MessageInfo v in value)
133 public void AddRange (MessageCollection value)
135 foreach (MessageInfo v in value)
139 public bool Contains (MessageInfo value)
141 return InnerList.Contains (value);
144 public void CopyTo (MessageInfo[] array, int index)
146 InnerList.CopyTo (array, index);
149 public int IndexOf (MessageInfo value)
151 return InnerList.IndexOf (value);
154 public void Insert (int index, MessageInfo value)
156 InnerList.Insert (index, value);
159 public void Remove (MessageInfo value)
161 InnerList.Remove (value);
164 public IEnumerator GetEnumerator ()
166 return InnerList.GetEnumerator ();
170 sealed class AssemblyCheckInfo : MarshalByRefObject {
171 private MessageCollection errors = new MessageCollection ();
172 private MessageCollection warnings = new MessageCollection ();
174 public MessageCollection Errors {
178 public MessageCollection Warnings {
179 get {return warnings;}
182 private XmlDocument[] mono_configs = new XmlDocument [0];
183 private IDictionary assembly_configs = new Hashtable ();
185 public void SetInstallationPrefixes (string[] prefixes)
187 mono_configs = new XmlDocument [prefixes.Length];
188 for (int i = 0; i < mono_configs.Length; ++i) {
189 mono_configs [i] = new XmlDocument ();
190 mono_configs [i].Load (Path.Combine (prefixes [i], "etc/mono/config"));
194 public string GetDllmapEntry (string assemblyPath, string library)
196 string xpath = "/configuration/dllmap[@dll=\"" + library + "\"]";
198 XmlDocument d = GetAssemblyConfig (assemblyPath);
200 XmlNode map = d.SelectSingleNode (xpath);
202 return map.Attributes ["target"].Value;
204 foreach (XmlDocument config in mono_configs) {
205 XmlNode map = config.SelectSingleNode (xpath);
207 return map.Attributes ["target"].Value;
212 private XmlDocument GetAssemblyConfig (string assemblyPath)
214 XmlDocument d = null;
215 if (assembly_configs.Contains (assemblyPath)) {
216 d = (XmlDocument) assembly_configs [assemblyPath];
219 string _config = assemblyPath + ".config";
220 if (File.Exists (_config)) {
221 d = new XmlDocument ();
224 assembly_configs.Add (assemblyPath, d);
230 sealed class AssemblyChecker : MarshalByRefObject {
232 public void CheckFile (string file, AssemblyCheckInfo report)
235 Check (Assembly.LoadFile (file), report);
237 catch (FileNotFoundException e) {
238 report.Errors.Add (new MessageInfo (null, null,
239 "Could not load `" + file + "': " + e.Message));
243 public void CheckWithPartialName (string partial, AssemblyCheckInfo report)
250 a = Assembly.LoadWithPartialName (p);
251 retry = p.EndsWith (".dll");
253 p = p.Substring (0, p.Length-4);
255 } while (a == null && retry);
258 report.Errors.Add (new MessageInfo (null, null,
259 "Could not load assembly reference `" + partial + "'."));
266 private void Check (Assembly a, AssemblyCheckInfo report)
268 foreach (Type t in a.GetTypes ()) {
273 private void Check (Type type, AssemblyCheckInfo report)
275 BindingFlags bf = BindingFlags.Instance | BindingFlags.Static |
276 BindingFlags.Public | BindingFlags.NonPublic;
278 foreach (MemberInfo mi in type.GetMembers (bf)) {
279 CheckMember (type, mi, report);
283 private void CheckMember (Type type, MemberInfo mi, AssemblyCheckInfo report)
285 DllImportAttribute[] attributes = null;
286 MethodBase[] methods = null;
287 switch (mi.MemberType) {
288 case MemberTypes.Constructor: case MemberTypes.Method: {
289 MethodBase mb = (MethodBase) mi;
290 attributes = new DllImportAttribute[]{GetDllImportInfo (mb)};
291 methods = new MethodBase[]{mb};
294 case MemberTypes.Event: {
295 EventInfo ei = (EventInfo) mi;
296 MethodBase add = ei.GetAddMethod (true);
297 MethodBase remove = ei.GetRemoveMethod (true);
298 attributes = new DllImportAttribute[]{
299 GetDllImportInfo (add), GetDllImportInfo (remove)};
300 methods = new MethodBase[]{add, remove};
303 case MemberTypes.Property: {
304 PropertyInfo pi = (PropertyInfo) mi;
305 MethodInfo[] accessors = pi.GetAccessors (true);
306 if (accessors == null)
308 attributes = new DllImportAttribute[accessors.Length];
309 methods = new MethodBase [accessors.Length];
310 for (int i = 0; i < accessors.Length; ++i) {
311 attributes [i] = GetDllImportInfo (accessors [i]);
312 methods [i] = accessors [i];
317 if (attributes == null || methods == null)
320 for (int i = 0; i < attributes.Length; ++i) {
321 if (attributes [i] == null)
323 CheckLibrary (methods [i], attributes [i], report);
327 private static DllImportAttribute GetDllImportInfo (MethodBase method)
332 if ((method.Attributes & MethodAttributes.PinvokeImpl) == 0)
335 // .NET 2.0 synthesizes pseudo-attributes such as DllImport
336 DllImportAttribute dia = (DllImportAttribute) Attribute.GetCustomAttribute (method,
337 typeof(DllImportAttribute), false);
341 // We're not on .NET 2.0; assume we're on Mono and use some internal
343 Type MonoMethod = Type.GetType ("System.Reflection.MonoMethod", false);
344 if (MonoMethod == null) {
347 MethodInfo GetDllImportAttribute =
348 MonoMethod.GetMethod ("GetDllImportAttribute",
349 BindingFlags.Static | BindingFlags.NonPublic);
350 if (GetDllImportAttribute == null) {
353 IntPtr mhandle = method.MethodHandle.Value;
354 return (DllImportAttribute) GetDllImportAttribute.Invoke (null,
355 new object[]{mhandle});
358 private void CheckLibrary (MethodBase method, DllImportAttribute attribute,
359 AssemblyCheckInfo report)
361 string library = attribute.Value;
362 string entrypoint = attribute.EntryPoint;
363 string type = method.DeclaringType.FullName;
364 string mname = method.Name;
369 Trace.WriteLine ("Trying to load base library: " + library);
371 foreach (string name in GetLibraryNames (method.DeclaringType, library, report)) {
372 if (LoadLibrary (type, mname, name, entrypoint, report, out error)) {
379 report.Errors.Add (new MessageInfo (
381 "Could not load library `" + library + "': " + error));
385 // UnixFileInfo f = new UnixFileInfo (soname);
386 if (found.EndsWith (".so")) {
387 report.Warnings.Add (new MessageInfo (type, mname,
388 string.Format ("Library `{0}' might be a development library",
393 [DllImport ("libgmodule-2.0.so")]
394 private static extern IntPtr g_module_open (string filename, int flags);
395 private static int G_MODULE_BIND_LAZY = 1 << 0;
396 private static int G_MODULE_BIND_LOCAL = 1 << 1;
397 // private static int G_MODULE_BIND_MASK = 0x03;
399 [DllImport ("libgmodule-2.0.so")]
400 private static extern int g_module_close (IntPtr handle);
402 [DllImport ("libgmodule-2.0.so")]
403 private static extern IntPtr g_module_error ();
405 [DllImport ("libgmodule-2.0.so")]
406 private static extern IntPtr g_module_name (IntPtr h);
408 [DllImport ("libgmodule-2.0.so")]
409 private static extern IntPtr g_module_build_path (
410 string directory, string module_name);
412 [DllImport ("libgmodule-2.0.so")]
413 private static extern int g_module_symbol (IntPtr module,
414 string symbol_name, out IntPtr symbol);
416 [DllImport ("libglib-2.0.so")]
417 private static extern void g_free (IntPtr mem);
419 private static string[] GetLibraryNames (Type type, string library, AssemblyCheckInfo report)
421 // TODO: keep in sync with
422 // mono/metadata/loader.c:mono_lookup_pinvoke_call
423 ArrayList names = new ArrayList ();
425 string dll_map = report.GetDllmapEntry (type.Assembly.Location, library);
430 int _dll_index = library.LastIndexOf (".dll");
432 names.Add (library.Substring (0, _dll_index));
434 if (!library.StartsWith ("lib"))
435 names.Add ("lib" + library);
437 IntPtr s = g_module_build_path (null, library);
438 if (s != IntPtr.Zero) {
440 names.Add (Marshal.PtrToStringAnsi (s));
447 s = g_module_build_path (".", library);
448 if (s != IntPtr.Zero) {
450 names.Add (Marshal.PtrToStringAnsi (s));
457 return (string[]) names.ToArray (typeof(string));
460 private static bool LoadLibrary (string type, string member,
461 string library, string symbol, AssemblyCheckInfo report, out string error)
464 IntPtr h = g_module_open (library,
465 G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
467 Trace.WriteLine (" Trying library name: " + library);
468 if (h != IntPtr.Zero) {
469 string soname = Marshal.PtrToStringAnsi (g_module_name (h));
470 Trace.WriteLine ("Able to load library " + library +
471 "; soname=" + soname);
473 if (g_module_symbol (h, symbol, out ignore) == 0)
474 report.Errors.Add (new MessageInfo (
476 string.Format ("library `{0}' is missing symbol `{1}'",
480 error = Marshal.PtrToStringAnsi (g_module_error ());
481 Trace.WriteLine ("\tError loading library `" + library + "': " + error);
484 if (h != IntPtr.Zero)
491 class MyOptions : Options {
492 [Option (int.MaxValue, "Assemblies to load by partial names (e.g. from the GAC)", 'r')]
493 public string[] references = new string[]{};
495 [Option (int.MaxValue, "Mono installation prefixes (for $prefix/etc/mono/config)", 'p')]
496 public string[] prefixes = new string[]{};
500 [DllImport ("does-not-exist")]
501 private static extern void Foo ();
503 [DllImport ("another-native-lib")]
504 private static extern void Bar ();
506 public static void Main (string[] args)
508 MyOptions o = new MyOptions ();
509 o.ProcessArgs (args);
511 AssemblyChecker checker = new AssemblyChecker ();
512 AssemblyCheckInfo report = new AssemblyCheckInfo ();
513 if (o.prefixes.Length == 0) {
514 // SystemConfigurationFile is $sysconfdir/mono/VERSION/machine.config
515 // We want $sysconfdir
516 DirectoryInfo configDir =
517 new FileInfo (RuntimeEnvironment.SystemConfigurationFile).Directory.Parent.Parent.Parent;
518 o.prefixes = new string[]{configDir.ToString ()};
520 report.SetInstallationPrefixes (o.prefixes);
521 foreach (string assembly in o.RemainingArguments) {
522 checker.CheckFile (assembly, report);
525 foreach (string assembly in o.references) {
526 checker.CheckWithPartialName (assembly, report);
529 foreach (MessageInfo m in report.Errors) {
530 PrintMessage ("error", m);
533 foreach (MessageInfo m in report.Warnings) {
534 PrintMessage ("warning", m);
538 private static void PrintMessage (string type, MessageInfo m)
540 Console.Write ("{0}: ", type);
542 Console.Write ("in {0}", m.Type);
543 if (m.Member != null)
544 Console.Write (".{0}: ", m.Member);
545 Console.WriteLine (m.Message);