Welcome to MSDN Blogs Sign in | Join | Help

Channel 9 Interview

Charles Torre and Robert Scoble (behind the camera) dropped by my office to chat with me about the loader for MSDN's Channel 9: part I and part II.
Also, check out the PDC video I was in for Channel 9: http://channel9.msdn.com/ShowPost.aspx?PostID=79591
Posted by Suzanne Cook | 19 Comments
Filed under:

New Assembly, Old .NET (and Vice-Versa)

I typically recommend that you build and test your assemblies against the same version of .NET that you will be running them against. That way, you'll have correct references and avoid surprises from behavior differences between builds.

Older assembly, newer .NET

But, sometimes you don't run against the same version that you built against. For example, the latest available CLR is the default when interop causes it to be loaded. Or, if the version of the CLR the assembly was built against is not available, the latest version can also be used instead. If there is a compatibility problem with that for your application, you can force the use of a preferred CLR version by using an app.config.

Compatibility

Yes, there are going to be behavior differences between versions - no way around it. It is the .NET team's goal to be as backwards-compatible as realistically possible between versions. But, some changes are required - new features are added which sometimes require (hopefully minimal) behavior changes in other features. If we were required to never break any app, ever, we wouldn't be able to fix any bugs or add any features for fear that someone, somewhere relied on the broken behavior in some strange way. As Dll Hell has shown in the past, new versions of components aren't really compatible. The only guaranteed 100% compatible version is the same one you tested against.

V2+ assembly, older .NET

Say you see a BadImageFormatException for hresult 0x80131107 (CLDB_E_FILE_OLDVER) with message "Version [...] is not a compatible version." That means that you've tried to load an assembly built against a newer version of the CLR than was running.

This fails because new files can't be opened on an older CLR that does not understand them. You will need to either use the v2 CLR to load the v2/v1 assemblies, or load only v1 assemblies on the v1 CLR. That's just good practice, anyway. If you try to use an older .NET than you built against, a method you built against may not exist, etc.

Posted by Suzanne Cook | 15 Comments
Filed under:

Versioning/Deploying Unmanaged Files

An unmanaged dll can be wrapped in a managed assembly by adding it as a file of a multi-module assembly. Then, it can be deployed and versioned in the same way as managed assemblies. (So, that assembly could contain nothing but metadata and unmanaged code - no managed code, if you prefer. It can also contain multiple unmanaged files in the same assembly.)

If your compiler does not support this directly, you can get this to work by adding that file as a linked managed resource. For example, see Visual Studio's /linkresource option (if using it for command line compiling).

This is useful in the case where DllImport() is used to access a function in that file. That call should be updated with the new assembly's info. For example, in the place of "unmanagedfile.dll", change it to include the display name like this: "unmanagedfile.dll, managedassembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089".

If that unmanaged file needs to be loaded by LoadLibrary() outside of DllImport(), however, it will need to follow the rules of LoadLibrary(), like the usual unmanaged file use outside the CLR. (See Junfeng's blog for an extra tip regarding that when using v2.)

Posted by Suzanne Cook | 4 Comments
Filed under:

Load(AssemblyName)

Calling Load(AssemblyName) is not necessarily the same as calling Load(String). If the AssemblyName.CodeBase is not set, then they do do the same thing. So, if you've set the AssemblyName.Name, CultureInfo, public key token / public key and/or Version properties, it would be the same as if you had specified those properties in a String (as a display name) and passed that to Load(String).

If the CodeBase is set, but the Name is not, however, then it's the same as calling Assembly.LoadFrom() on that CodeBase.

When both the CodeBase and the Name are set, then the bind is tried with all the given binding information except the CodeBase (so, again, just like calling Load(String)). If that succeeds, we're done. But, if that fails, then the bind is tried again with just the CodeBase (just like LoadFrom()). If it fails again, then, of course, the whole bind fails. But, if it succeeds, then we verify that the binding properties in the AssemblyName match the found assembly. If they don't match, a FileLoadException will be thrown for hresult FUSION_E_REF_DEF_MISMATCH.

So, setting both the CodeBase and the Name is useful for when you want to both load an assembly at a given path into the LoadFrom context, and verify that it has the public key token, etc. that you expect. Of course, as described above (and due to binding context rules), keep in mind that just because you call Load(AssemblyName) with a CodeBase, it does not mean that it will be loaded from that path.

Posted by Suzanne Cook | 7 Comments
Filed under:

Debugging an InvalidCastException

First, obviously, find the two types for which the cast failed and verify that they are the same type or otherwise castable.

Next, if the type was just deserialized, also verify that its assembly successfully loaded in the target appdomain.

If everything seems fine, check to see if the assemblies for those two types are loaded from different locations and in the same appdomain. (The actual cast is done in just one appdomain, even if the exception happens when passing a type between two appdomains.) Even if the bits of those assemblies are totally identical, if they are loaded from different paths, they will be considered different, so their types will be considered different. (See Comparing Already-Loaded Assemblies.)

A quick way to check for that is to examine the loaded module window of a debugger to see if that assembly was loaded multiple times. If it was, break on module loads to get the callstack for the unexpected load. If that's inconvenient, try getting the Fusion log.

Usually, the problem is that:

  1. The assembly is available in the GAC (or the ApplicationBase) and loaded there by static reference (something was compiled against that assembly).
  2. It has also been loaded dynamically by path, from another path (LoadFrom(), LoadFile(), etc.).
  3. Then, the code tries to cast the type from (2) to the corresponding type from (1).

To fix this, once you find the offending caller, you will need to either cause the two types to be loaded from the same assembly from the exact same path, or avoid doing the cast. To decide between the assemblies at paths (1) and (2), see Choosing a Binding Context. Usually, I recommend using (1) - see Switching to the Load Context for help with implementing that.

App.config Examples

Below are three examples of useful application configuration files.

  1. Forces the v1.0 CLR to be run. If the v1.0 CLR is not installed, the app will fail to run.

    <?xml version ="1.0"?>
     <configuration>
        <startup>
             <requiredRuntime version="v1.0.3705"/>
             <supportedRuntime version="v1.0.3705"/>
         </startup>
     </configuration>

  2. Redirects “assemblyName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=25283151a234958d“ to version 2.0.0.0 of that assembly. This is only useful for strongly-named assemblies, since versions don't matter for those that are simply-named.

    <?xml version ="1.0"?>
    <configuration>
    <runtime>

            <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

                    <dependentAssembly>
                            <assemblyIdentity name="assemblyName" culture="" publicKeyToken="25283151a234958d"/>
                            <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>

                    </dependentAssembly>
            </assemblyBinding>

    </runtime>
    </configuration>

  3. Redirects “assemblyName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=8968ee41e78ce97a“ to codebase “http://www.yourwebsite.com/filename.dll“. 'Href' can also be set to something like “file:///c:/localfile/filename.dll“. Note that redirecting to a codebase causes a System.Net.WebPermission or System.IO.FileIOPermissionAccess.Read + PathDiscovery demand when loading that assembly.

    <?xml version ="1.0"?>
    <configuration>
    <runtime>

            <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

                    <dependentAssembly>
                            <assemblyIdentity name="assemblyName" culture="" publicKeyToken="8968ee41e78ce97a"/>
                            <codeBase version="1.0.0.0" href="http://www.yourwebsite.com/filename.dll"/>

                    </dependentAssembly>
            </assemblyBinding>

    </runtime>
    </configuration>

    Posted by Suzanne Cook | 20 Comments
    Filed under:

    Determining Whether a File Is an Assembly

    A file is an assembly if and only if it's managed and it contains an Assembly entry in its CLR metadata.

    Determining by hand

    A fast way to determine whether a file is an assembly is to run ildasm.exe on it. If it immediately gives an error saying that it may not be a PE file, then it's not a managed file. But, if it is an assembly, then ildasm will show an entry for the Assembly definition (“.assembly“ in the MANIFEST window or at the bottom of the original window).

    Determining programmatically

    From unmanaged code, you can call GetAssemblyFromScope() on the IMetaDataAssemblyImport interface for the file. If it returns S_OK, it's an assembly. If it returns CLDB_E_RECORD_NOTFOUND, it's not an assembly.

    From managed code, if AssemblyName.GetAssemblyName(), Assembly.Load*(), etc. succeeds when given that file, then it's an assembly. If the load failed with a BadImageFormatException, then it may not be an assembly. There are other reasons, however, why that exception may have been thrown (maybe it's an assembly but could not be loaded because it has an incorrect format). Coming soon in v2, if the hresult for BadImageFormatException is COR_E_ASSEMBLYEXPECTED, then it's because it's not an assembly. Catch the exception and call System.Runtime.InteropServices.Marshal.GetHRForException() to get its hresult to find out.

    Note that this assumes that you are not concerned about performance. If you are concerned about that, then the way to optimize for it is to not try to determine whether files are assemblies at runtime at all. (Some people are concerned about the exception that is thrown if the file is not an assembly. They overlook that in order to determine that it is not one, it has to be loaded and then thrown away, which can be even more expensive than catching an exception!) Instead, require that only assemblies are given to you. Then, if a file is not an assembly, it will be an exceptional situation. That way, you avoid the bad perf of both the exception and the unnecessary file loading.

    Posted by Suzanne Cook | 10 Comments
    Filed under:

    Debugging a MissingMethodException, MissingFieldException, TypeLoadException

    Say you've just installed some assemblies from a third party and now you're seeing a MissingMethodException, MissingFieldException, or TypeLoadException (during the run of an application using those assemblies). Below are the common causes.

    Loading failures
    First, check for assembly binding failures by getting the Fusion log. Look for the assembly containing that method/field/type or assemblies containing types referenced by it. If an assembly failed to load, use the instructions at the same link to help resolve that issue.

    Unexpected assembly version loaded
    But, if that's not the problem, turn on the Fusion log for everything - not just failures - and check to see if the wrong version of those assemblies is being loaded. Look at the display names requested. Are any requesting outdated versions, even after policy has been applied (see further down in the log for the post-policy display name)? If so, you may want to recompile part of your app so that it has current references.

    Loaded from unexpected path
    If that doesn't help, run filever.exe on the file at the path it was loaded from. You can get that from the loaded modules list in a debugger. It's also the last path listed in the Fusion log, for a successful log (if it's in the GAC, no path is listed). Make sure it is the same path as you would expect.

    Not in this version of the assembly
    Next, run ildasm.exe on the file at the path it was loaded from at runtime. Make sure that the method/field/type is there and is defined how you would expect it. Maybe the file has changed, adding or removing methods or the like, but the assembly version and location have stayed the same.

    Invalid IL
    Now, try running peverify.exe on the file containing the method/field/type using the path it was loaded from at runtime. If it gives an error, check back to see if there was a warning at compile-time for this file.

    Where to Find Technical Support

    Microsoft's official support website is http://support.microsoft.com/. It has all kinds of resources like product FAQs, downloads, searchable KB articles, newsgroup pointers, and ways to reach people to help with your individual needs or feedback.

    Unfortunately, I can't give attention to individual customer issues. That's because I work in product design and development, not customer support. Someone has to be focused on that, or else we'd never ship anything! So, I'm going to have to leave your questions and comments to MS's official channels which specialize in that. Please keep comments you post here general and about the loader or performance.

    Determining the Referencing Assembly

    Say you're debugging your application and you see that version 1.0 of an assembly is being loaded when you thought it should be version 2.0. Where is the reference to 1.0 coming from?

    The easiest way to find out is to look at the Fusion log for this bind. If the version 1.0 assembly was successfully loaded, use the ForceLog/"Log all binds" option of FusLogVw. Then, look for the line in the log showing the calling assembly:

    Calling assembly : referencingAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=12ab3bf24c56c45b.

    It shows the display name of the calling assembly when available. It doesn't tell you whether this is a static or a dynamic reference because Fusion doesn't know or care (that doesn't matter for binding purposes). So, this could mean that referencingAssembly was built against the other 1.0 assembly, or that it asked for it at runtime via Assembly.Load(), etc.

    Sometimes the calling assembly is not specified in the log. There are a few possible cases where that happens:

    • The assembly was requested by unmanaged code (interop).
    • The calling assembly was in another appdomain (AppDomain.CreateInstance(), etc.).
    • The calling assembly had not been loaded through Fusion (Assembly.Load(byte[]), Assembly.LoadFile(), etc.).

     

    LoadFile vs. LoadFrom

    Be careful - these aren't the same thing.

    • LoadFrom() goes through Fusion and can be redirected to another assembly at a different path but with that same identity if one is already loaded in the LoadFrom context.
    • LoadFile() doesn't bind through Fusion at all - the loader just goes ahead and loads exactly* what the caller requested. It doesn't use either the Load or the LoadFrom context.

    So, LoadFrom() usually gives you what you asked for, but not necessarily. LoadFile() is for those who really, really want exactly what is requested. (*However, starting in v2, policy will be applied to both LoadFrom() and LoadFile(), so LoadFile() won't necessarily be exactly what was requested. Also, starting in v2, if an assembly with its identity is in the GAC, the GAC copy will be used instead. Use ReflectionOnlyLoadFrom() to load exactly what you want - but, note that assemblies loaded that way can't be executed.)

    LoadFile() has a catch. Since it doesn't use a binding context, its dependencies aren't automatically found in its directory. If they aren't available in the Load context, you would have to subscribe to the AssemblyResolve event in order to bind to them.

    So, which one should you use? That depends on which binding context is right for you and whether it matters that the LoadFrom context may redirect the bind. Regarding the latter:

    • If no other code can load assemblies in your AppDomain, your LoadFrom() call can prevent a redirect* by just not loading anything else with a duplicate identity.
    • If you do need to load a duplicate, you could get away with it by loading it into anything other than the LoadFrom context (or into another AppDomain) instead.
    • But, maybe the redirect is really okay. Are they identical assemblies, just at different paths? If yes, it may be safe to risk the redirect unless its dependencies aren't available in the dir of the first one loaded.
    • Or, are they possibly different bits, just with the same assembly identity? If so, it may be unsafe to risk the redirect: maybe you'll get an more-updated/not-as-updated version which you didn't expect. Or, if it's not strongly-named, maybe this is a third-party's assembly, totally unrelated to the one you expect.
    Posted by Suzanne Cook | 9 Comments
    Filed under:

    LoadFrom's Second Bind

    Pre-v2, when you load an assembly by path through Fusion (LoadFrom(), ExecuteAssembly(), etc.), it can actually cause two binds, not just one. The first bind loads the file at the given path. If that is successful, another bind is done with the display name of that assembly to see if it's available in the Load context. If it's not available, the LoadFrom() call is still successful - we throw away the results of the second bind. If it is available, we check the path returned from the first bind against the path from the second bind. If they're identical, we keep the Load context IAssembly and throw away the LoadFrom one. (That means that that assembly will be in the Load context, not the LoadFrom context.) Otherwise, we keep the LoadFrom context IAssembly.

    This is to ensure that the Load context remains order-independent - an important part of the design for that context. It's an implementation detail, but I bring it up because it has some side-effects that are surprising to customers:

    • The second bind shows up in the Fusion log. (It's just another bind to Fusion.) It's unsettling to see a failure in the log (for the second bind) when the LoadFrom() call succeeded.
    • Doing a second bind by display name causes probing which slows down the performance of LoadFrom(), especially over http, when the file isn't found immediately.

    Fusion has stopping doing the second bind in v2. But, when using an older CLR, customers can avoid both side-effects by putting a redirect from the display name to the codebase in a config file. That will skip probing, helping perf, and, assuming it's the correct codebase, avoid a Fusion log noting an ignorable failure.

    Posted by Suzanne Cook | 2 Comments
    Filed under:

    Avoid DevPath

    I hesitate to talk about this because I don't want people who don't know about it to think, "Hey, what's this DevPath thing? I need that." But, maybe if I don't explain how to use it, it will be too much effort for people who don't already know how. :) (And, for those who already know how and are, in fact, using it, hopefully, they'll see this and get off that plan.)

    The intent of DevPath was to make the development environment less painful. Assemblies could be put there and bound to at runtime, ignoring the assembly version and overriding the GAC.

    It turns out that that's badness for several reasons (below). So, DevPath is soon to be deprecated. Don't use it - not even in the development environment.

    Why It Should Never Be Used
    Versioning alone is why you should never, ever use it in a production environment. It subscribes your users to dll hell. See Avoid Partial Binds for details (DevPath causes partial binding, since the version is ignored for the bind).

    It's not good for the dev. env., either - it makes it unnecessarily different from the shipping env., which may lead to uncaught versioning or deployment bugs in the shipping env.

    What to Do Instead
    If DevPath was a development environment-only solution for you:
    I strongly recommend not changing your assembly versions between non-shipping builds instead of using DevPath.

    If you are shipping code relying on DevPath:
    One thing you could do is create a new AppDomain with the ApplicationBase set to the path you care about. Once you do that, see Executing Code in Another Appdomain.

    If there are multiple paths you want to load assemblies from, use PrivateBinPath or multiple AppDomains. If that's not reasonable for you, consider using the LoadFrom context.

    Posted by Suzanne Cook | 16 Comments
    Filed under:

    ReflectionTypeLoadException

    If a type can't be loaded for some reason during a call to Module.GetTypes(), ReflectionTypeLoadException will be thrown. Assembly.GetTypes() also throws this because it calls Module.GetTypes().

    The Message for this exception is "One or more exceptions have been thrown while loading the types" or "Unable to load one or more of the types in the assembly", which doesn't seem very descriptive. But, the exception actually provides more info that that. Just get the LoaderExceptions property of the ReflectionTypeLoadException instance. That will give an array of the exceptions caught while loading all of the types from the module. If the exceptions are due to an assembly loading problem, see my general debugging advice.

    Binding to .NET Frameworks Assemblies

    By "Frameworks assemblies," I mean the assemblies that ship with the CLR. But, I'm not counting mscorlib.dll, since it's special in a different way.

    With v1.0 SP3 or later, Frameworks assemblies are unified. That means that the version of those assemblies that you request is ignored - you get the version that matches the loaded CLR. This only applies when the assembly is loaded by version - that is, by assembly display name or static reference (AssemblyRef in the CLR metadata).

    For example, if the v1.1 CLR is loaded and you request v1.0's system.dll, then you'll get the v1.1 system.dll back. If you load it by path from c:\foo\system.dll, however, then you'll get c:\foo\system.dll back, not the v1.1 system.dll.

    Those assemblies are unified because the owners of the code feel that they are so closely tied to the CLR/mscorlib.dll that they should not be loaded with a different version than they were built against. Additionally, there are some cases where they expect that only one copy of the Frameworks assemblies be loaded in a given process.

    If the v1.0 (pre-SP3) CLR is loaded, though, you will get the assembly that you ask for. Unification isn't done for Frameworks assemblies in that version.

    Do not rely on this behavior. You should still fully-specify assembly references with the correct version. That's just good practice, in general. Besides, if your assembly is loaded in a CLR later than v2.0, those references may not be unified, so you may get unexpected behavior.

    Posted by Suzanne Cook | 3 Comments
    Filed under:
    More Posts Next page »
     
    Page view tracker