2005-06-05 Peter Bartok <pbartok@novell.com>
[mono.git] / mcs / tools / mono-shlib-cop / mono-shlib-cop.cs
1 //
2 // mono-shlib-cop.cs: Check unmanaged dependencies
3 //
4 // Compile as:
5 //    mcs mono-shlib-cop.cs -r:Mono.Posix -r:Mono.GetOptions
6 //
7 // Authors:
8 //  Jonathan Pryor (jonpryor@vt.edu)
9 //
10 // (C) 2005 Jonathan Pryor
11 //
12
13 //
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:
21 // 
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 // 
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.
32 //
33
34 //
35 // About:
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
40 //        angering users)
41 //      - DllImporting a symbol which doesn't exist in the target library
42 //      - etc.
43 //
44 // Implementation:
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.
50 //    - Thus, algorithm:
51 //      - Create AppDomain with ApplicationBase path set to directory assembly
52 //        resides in
53 //      - Create an AssemblyChecker instance within the AppDomain
54 //      - Check an assembly with AssemblyChecker; store results in Report.
55 //      - Print results.
56 //
57 // TODO:
58 //    - AppDomain use
59 //    - specify $prefix to use for finding $prefix/etc/mono/config
60 //    - dllmap caching?  (Is it possible to avoid reading the .config file
61 //      into each AppDomain at least once?  OS file caching may keep perf from
62 //      dieing with all the potential I/O.)
63 //    - Make -r work correctly (-r:Mono.Posix should read Mono.Posix from the
64 //      GAC and inspect it.)
65 //
66 #define TRACE
67
68 using System;
69 using System.Collections;
70 using System.Diagnostics;
71 using System.IO;
72 using System.Reflection;
73 using System.Runtime.InteropServices;
74 using System.Xml;
75
76 using Mono.GetOptions;
77 using Mono.Unix;
78
79 [assembly: AssemblyTitle ("mono-shlib-cop")]
80 [assembly: AssemblyCopyright ("(C) 2005 Jonathan Pryor")]
81 [assembly: AssemblyDescription ("Looks up shared library dependencies of managed code")]
82 [assembly: Mono.Author ("Jonathan Pryor")]
83 [assembly: Mono.UsageComplement ("[ASSEMBLY]+ [-r:ASSEMBLY_REF]+")]
84 [assembly: Mono.ReportBugsTo ("jonpryor@vt.edu")]
85
86 namespace Mono.Unmanaged.Check {
87         [Serializable]
88         sealed class MessageInfo {
89                 public string Type;
90                 public string Member;
91                 public string Message;
92
93                 public MessageInfo (string type, string member, string message)
94                 {
95                         Type = type;
96                         Member = member;
97                         Message = message;
98                 }
99
100                 public override bool Equals (object value)
101                 {
102                         MessageInfo other = value as MessageInfo;
103                         if (other == null)
104                                 return false;
105
106                         return Type == other.Type && Member == other.Member && 
107                                 Message == other.Message;
108                 }
109
110                 public override int GetHashCode ()
111                 {
112                         return Type.GetHashCode () ^ Member.GetHashCode () ^ 
113                                 Message.GetHashCode ();
114                 }
115         }
116
117         sealed class MessageCollection : MarshalByRefObject {
118                 private ArrayList InnerList = new ArrayList ();
119
120                 public MessageCollection ()
121                 {
122                 }
123
124                 public int Add (MessageInfo value)
125                 {
126                         if (!InnerList.Contains (value))
127                                 return InnerList.Add (value);
128                         return InnerList.IndexOf (value);
129                 }
130
131                 public void AddRange (MessageInfo[] value)
132                 {
133                         foreach (MessageInfo v in value)
134                                 Add (v);
135                 }
136
137                 public void AddRange (MessageCollection value)
138                 {
139                         foreach (MessageInfo v in value)
140                                 Add (v);
141                 }
142
143                 public bool Contains (MessageInfo value)
144                 {
145                         return InnerList.Contains (value);
146                 }
147
148                 public void CopyTo (MessageInfo[] array, int index)
149                 {
150                         InnerList.CopyTo (array, index);
151                 }
152
153                 public int IndexOf (MessageInfo value)
154                 {
155                         return InnerList.IndexOf (value);
156                 }
157
158                 public void Insert (int index, MessageInfo value)
159                 {
160                         InnerList.Insert (index, value);
161                 }
162
163                 public void Remove (MessageInfo value)
164                 {
165                         InnerList.Remove (value);
166                 }
167
168                 public IEnumerator GetEnumerator ()
169                 {
170                         return InnerList.GetEnumerator ();
171                 }
172         }
173
174         sealed class Report : MarshalByRefObject {
175                 private MessageCollection errors   = new MessageCollection ();
176                 private MessageCollection warnings = new MessageCollection ();
177
178                 public MessageCollection Errors {
179                         get {return errors;}
180                 }
181
182                 public MessageCollection Warnings {
183                         get {return warnings;}
184                 }
185         }
186
187         sealed class AssemblyChecker : MarshalByRefObject {
188
189                 public void CheckFile (string file, Report report)
190                 {
191                         try {
192                                 Check (Assembly.LoadFile (file), report);
193                         }
194                         catch (FileNotFoundException e) {
195                                 report.Errors.Add (new MessageInfo (null, null, 
196                                         "Could not load `" + file + "': " + e.Message));
197                         }
198                 }
199
200                 public void CheckWithPartialName (string partial, Report report)
201                 {
202                         AssemblyName an = new AssemblyName ();
203                         an.Name = partial;
204                         try {
205                                 Assembly a = Assembly.Load (an);
206                                 Check (a, report);
207                         }
208                         catch (FileNotFoundException e) {
209                                 report.Errors.Add (new MessageInfo (null, null, 
210                                         "Could not load assembly reference `" + partial + "': " + e.Message));
211                         }
212                 }
213
214                 private void Check (Assembly a, Report report)
215                 {
216                         foreach (Type t in a.GetTypes ()) {
217                                 Check (t, report);
218                         }
219                 }
220
221                 private void Check (Type type, Report report)
222                 {
223                         BindingFlags bf = BindingFlags.Instance | BindingFlags.Static | 
224                                 BindingFlags.Public | BindingFlags.NonPublic;
225
226                         foreach (MemberInfo mi in type.GetMembers (bf)) {
227                                 CheckMember (type, mi, report);
228                         }
229                 }
230
231                 private void CheckMember (Type type, MemberInfo mi, Report report)
232                 {
233                         DllImportAttribute[] attributes = null;
234                         MethodBase[] methods = null;
235                         switch (mi.MemberType) {
236                                 case MemberTypes.Constructor: case MemberTypes.Method: {
237                                         MethodBase mb = (MethodBase) mi;
238                                         attributes = new DllImportAttribute[]{GetDllImportInfo (mb)};
239                                         methods = new MethodBase[]{mb};
240                                         break;
241                                 }
242                                 case MemberTypes.Event: {
243                                         EventInfo ei = (EventInfo) mi;
244                                         MethodBase add = ei.GetAddMethod (true);
245                                         MethodBase remove = ei.GetRemoveMethod (true);
246                                         attributes = new DllImportAttribute[]{
247                                                 GetDllImportInfo (add), GetDllImportInfo (remove)};
248                                         methods = new MethodBase[]{add, remove};
249                                         break;
250                                 }
251                                 case MemberTypes.Property: {
252                                         PropertyInfo pi = (PropertyInfo) mi;
253                                         MethodInfo[] accessors = pi.GetAccessors (true);
254                                         if (accessors == null)
255                                                 break;
256                                         attributes = new DllImportAttribute[accessors.Length];
257                                         methods = new MethodBase [accessors.Length];
258                                         for (int i = 0; i < accessors.Length; ++i) {
259                                                 attributes [i] = GetDllImportInfo (accessors [i]);
260                                                 methods [i] = accessors [i];
261                                         }
262                                         break;
263                                 }
264                         }
265                         if (attributes == null || methods == null)
266                                 return;
267
268                         for (int i = 0; i < attributes.Length; ++i) {
269                                 if (attributes [i] == null)
270                                         continue;
271                                 CheckLibrary (methods [i], attributes [i], report);
272                         }
273                 }
274
275                 private static DllImportAttribute GetDllImportInfo (MethodBase method)
276                 {
277                         if (method == null)
278                                 return null;
279
280                         if ((method.Attributes & MethodAttributes.PinvokeImpl) == 0)
281                                 return null;
282
283                         // .NET 2.0 synthesizes pseudo-attributes such as DllImport
284                         DllImportAttribute dia = (DllImportAttribute) Attribute.GetCustomAttribute (method, 
285                                                 typeof(DllImportAttribute), false);
286                         if (dia != null)
287                                 return dia;
288
289                         // We're not on .NET 2.0; assume we're on Mono and use some internal
290                         // methods...
291                         Type MonoMethod = Type.GetType ("System.Reflection.MonoMethod", false);
292                         if (MonoMethod == null) {
293                                 return null;
294                         }
295                         MethodInfo GetDllImportAttribute = 
296                                 MonoMethod.GetMethod ("GetDllImportAttribute", 
297                                                 BindingFlags.Static | BindingFlags.NonPublic);
298                         if (GetDllImportAttribute == null) {
299                                 return null;
300                         }
301                         IntPtr mhandle = method.MethodHandle.Value;
302                         return (DllImportAttribute) GetDllImportAttribute.Invoke (null, 
303                                         new object[]{mhandle});
304                 }
305                 
306                 private void CheckLibrary (MethodBase method, DllImportAttribute attribute, 
307                                 Report report)
308                 {
309                         string library = attribute.Value;
310                         string entrypoint = attribute.EntryPoint;
311                         string type = method.DeclaringType.FullName;
312                         string mname = method.Name;
313
314                         string found = null;
315                         string error = null;
316
317                         Trace.WriteLine ("Trying to load base library: " + library);
318
319                         foreach (string name in GetLibraryNames (method.DeclaringType, library)) {
320                                 if (LoadLibrary (type, mname, name, entrypoint, report, out error)) {
321                                         found = name;
322                                         break;
323                                 }
324                         }
325
326                         if (found == null) {
327                                 report.Errors.Add (new MessageInfo (
328                                                         type, mname,
329                                                         "Could not load library `" + library + "': " + error));
330                                 return;
331                         }
332
333                         // UnixFileInfo f = new UnixFileInfo (soname);
334                         if (found.EndsWith (".so")) {
335                                 report.Warnings.Add (new MessageInfo (type, mname, 
336                                                 string.Format ("Library `{0}' might be a development library",
337                                                         found)));
338                         }
339                 }
340
341                 [DllImport ("libgmodule-2.0.so")]
342                 private static extern IntPtr g_module_open (string filename, int flags);
343                 private static int G_MODULE_BIND_LAZY = 1 << 0;
344                 private static int G_MODULE_BIND_LOCAL = 1 << 1;
345                 // private static int G_MODULE_BIND_MASK = 0x03;
346
347                 [DllImport ("libgmodule-2.0.so")]
348                 private static extern int g_module_close (IntPtr handle);
349
350                 [DllImport ("libgmodule-2.0.so")]
351                 private static extern IntPtr g_module_error ();
352
353                 [DllImport ("libgmodule-2.0.so")]
354                 private static extern IntPtr g_module_name (IntPtr h);
355
356                 [DllImport ("libgmodule-2.0.so")]
357                 private static extern IntPtr g_module_build_path (
358                         string directory, string module_name);
359
360                 [DllImport ("libgmodule-2.0.so")]
361                 private static extern int g_module_symbol (IntPtr module, 
362                                 string symbol_name, out IntPtr symbol);
363
364                 [DllImport ("libglib-2.0.so")]
365                 private static extern void g_free (IntPtr mem);
366
367                 private static string[] GetLibraryNames (Type type, string library)
368                 {
369                         // TODO: keep in sync with
370                         // mono/metadata/loader.c:mono_lookup_pinvoke_call
371                         ArrayList names = new ArrayList ();
372
373                         string dll_map = GetDllMapEntry (type, library);
374                         if (dll_map != null) 
375                                 names.Add (dll_map);
376
377                         names.Add (library);
378                         int _dll_index = library.LastIndexOf (".dll");
379                         if (_dll_index >= 0)
380                                 names.Add (library.Substring (0, _dll_index));
381
382                         if (!library.StartsWith ("lib"))
383                                 names.Add ("lib" + library);
384
385                         IntPtr s = g_module_build_path (null, library);
386                         if (s != IntPtr.Zero) {
387                                 try {
388                                         names.Add (Marshal.PtrToStringAnsi (s));
389                                 }
390                                 finally {
391                                         g_free (s);
392                                 }
393                         }
394
395                         s = g_module_build_path (".", library);
396                         if (s != IntPtr.Zero) {
397                                 try {
398                                         names.Add (Marshal.PtrToStringAnsi (s));
399                                 }
400                                 finally {
401                                         g_free (s);
402                                 }
403                         }
404
405                         return (string[]) names.ToArray (typeof(string));
406                 }
407
408                 private static bool LoadLibrary (string type, string member, 
409                                 string library, string symbol, Report report, out string error)
410                 {
411                         error = null;
412                         IntPtr h = g_module_open (library, 
413                                         G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
414                         try {
415                                 Trace.WriteLine ("    Trying library name: " + library);
416                                 if (h != IntPtr.Zero) {
417                                         string soname = Marshal.PtrToStringAnsi (g_module_name (h));
418                                         Trace.WriteLine ("Able to load library " + library + 
419                                                         "; soname=" + soname);
420                                         IntPtr ignore;
421                                         if (g_module_symbol (h, symbol, out ignore) == 0)
422                                                 report.Errors.Add (new MessageInfo (
423                                                                 type, member,
424                                                                 string.Format ("library `{0}' is missing symbol `{1}'",
425                                                                         library, symbol)));
426                                         return true;
427                                 }
428                                 error = Marshal.PtrToStringAnsi (g_module_error ());
429                                 Trace.WriteLine ("\tError loading library `" + library + "': " + error);
430                         }
431                         finally {
432                                 if (h != IntPtr.Zero)
433                                         g_module_close (h);
434                         }
435                         return false;
436                 }
437
438                 private static XmlDocument etc_mono_config;
439
440                 static AssemblyChecker ()
441                 {
442                         etc_mono_config = new XmlDocument ();
443                         etc_mono_config.Load ("/etc/mono/config");
444                 }
445
446                 private static string GetDllMapEntry (Type type, string library)
447                 {
448                         string xpath = "//dllmap[@dll=\"" + library + "\"]";
449                         // string xpath = "//dllmap";
450                         string _config = type.Assembly.Location + ".config";
451                         if (File.Exists (_config)) {
452                                 Trace.WriteLine ("Reading .config file: " + _config);
453                                 XmlDocument config = new XmlDocument ();
454                                 config.Load (_config);
455                                 XmlNodeList entry = config.SelectNodes (xpath);
456                                 if (entry.Count > 0)
457                                         return entry[0].Attributes ["target"].Value;
458                         }
459                         Trace.WriteLine (".config not found; using /etc/mono/config");
460                         XmlNodeList maps = etc_mono_config.SelectNodes (xpath);
461                         Trace.WriteLine (string.Format ("{0} <dllmap/> entries found!", maps.Count));
462                         if (maps.Count > 0)
463                                 return maps[0].Attributes ["target"].Value;
464                         return null;
465                 }
466         }
467
468         class MyOptions : Options {
469                 [Option (int.MaxValue, "Assemblies to load by partial names (e.g. from the GAC)", 'r')]
470                 public string[] references = new string[]{};
471
472         }
473
474         class Runner {
475                 [DllImport ("does-not-exist")]
476                         private static extern void Foo ();
477
478                 [DllImport ("another-native-lib")]
479                         private static extern void Bar ();
480
481                 public static void Main (string[] args)
482                 {
483                         MyOptions o = new MyOptions ();
484                         o.ProcessArgs (args);
485
486                         AssemblyChecker checker = new AssemblyChecker ();
487                         Report report = new Report ();
488                         foreach (string assembly in o.RemainingArguments) {
489                                 checker.CheckFile (assembly, report);
490                         }
491
492                         foreach (string assembly in o.references) {
493                                 checker.CheckWithPartialName (assembly, report);
494                         }
495
496                         foreach (MessageInfo m in report.Errors) {
497                                 PrintMessage ("error", m);
498                         }
499
500                         foreach (MessageInfo m in report.Warnings) {
501                                 PrintMessage ("warning", m);
502                         }
503                 }
504
505                 private static void PrintMessage (string type, MessageInfo m)
506                 {
507                         Console.Write ("{0}: ", type);
508                         if (m.Type != null)
509                                 Console.Write ("in {0}", m.Type);
510                         if (m.Member != null)
511                                 Console.Write (".{0}: ", m.Member);
512                         Console.WriteLine (m.Message);
513                 }
514         }
515 }
516