Building p4api.net.dll for 'Any CPU'

2019-06-27 04:52发布

问题:

I got hit earlier this week with my first BadImageFormatException when trying to use the p4api.net.dll. Turns out my assumption that I could just use the 64-bit version of it and p4bridge.dll was incorrect!

In my investigation I noticed I had a warning:

warning MSB3270: There was a mismatch between the processor architecture of the project being built "MSIL" and the processor architecture of the reference "p4api.net, Version=2014.3.100.9144, Culture=neutral, PublicKeyToken=f6b9b9d036c873e1, processorArchitecture=AMD64", "AMD64"

I have learned that this means that the .dll I used is specified to use 64-bit, rather than being built with Any-CPU.

While we could change the project to be specifically x64, I have been asked to investigate if it's still possible to keep it as platform agnostic as possible. I am still new to both p4api.net and having to deal with this sort of dependency, but my understanding is that if I can build p4api.net.dll as 'Any CPU' the warning would go away, and I would just need to do some magic to make sure the correct p4bridge.dll was used depending on the CurrentPlatform I defined in the project.

I downloaded and compiled the p4api.net source and tried specifying any CPU, but it didn't work. Now it says that the architecture is set for x86 and I still get a similar MSB3270 error - now for x86. I don't get a warning like this with the p4api.net solution, however, so it doesn't appear to have any platform dependencies that I am aware of. But if I use CorFlags.exe on p4api.net.dll it is definitely giving PE/32BIT flags that are platform specific.

So to my questions:

  • Does anyone know offhand if it's even possible to build p4api.net for Any CPU?
  • Failing that, what would I have to do to examine what (if any) platform dependencies exist that would prevent me from building p4api.net.dll for Any CPU?

If the answers to the above are no I'll probably have new questions, but will cross that bridge when I get there! :)

Thanks in advance for any help/thoughts.

回答1:

I don't have the code with me at the moment, but I can describe to you what I did to solve this problem. The issue is that while the p4api.net library will compile just fine when set to target Any CPU, the underlying native C++ library (p4bridge.dll) is targeting either x86 or x64, and there's no way to compile it for both architectures in one DLL. Thus, I had to get clever!

To make this work, I added both versions of p4bridge.dll to the p4api.net project, renaming them p4bridge86.dll and p4bridge64.dll, and marked them to be included as assembly resources. Next, I wrote a static function in the p4api.net library that figured out which architecture the machine is running on, gets the correct p4bridge.dll resource, saves it to disk next to the p4api.net.dll that's currently executing, and finally P/Invokes the Windows LoadLibrary function on the extracted p4bridge.dll.

The final piece of the puzzle is making sure this function you've written runs before any types inside p4api.net are instantiated, as at that point the loader will see types referencing p4bridge.dll and attempt to load it from disk, and if you've never run the extraction function, it won't be there and you'll have an exception thrown. To fix this, I had to get kinda hacky with .NET: I downloaded Einar Egilsson's fantastic little tool InjectModuleInitializer and set up a post build step on the p4api.net project which ran the tool and had it insert instructions to call the static extractor/loader function I wrote before any other code in the module was executed.

With this setup, I had a single p4api.net assembly which was compiled for Any CPU, but could automatically deal with the fact that the required native p4bridge.dll must exist separately for the x86 and x64 architectures.

When I get home later, I'll see about adding source code showing exactly how I wrote the extract and load function, and anything else that might need more clarity. Sorry this answer came over a year after you asked originally, but I too needed to figure out a solution to this problem a couple days ago and since I managed to do it, I thought it would be worth sharing for anyone who might run into this very convoluted problem in the future!

Edit: Here's the implementation of the class that extracts and loads the correct p4bridge.dll. It only extracts the DLL if it either isn't extracted, or the one it does find fails to load (because perhaps it's the wrong architecture, for some reason). Also, p4bridge.dll is several megabytes in size and there's not much point in performing unnecessary IO every time p4api.net is loaded!

internal static class P4BridgeLoader
{
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string dllToLoad);

    private static void ExtractResource(string resourceName, string outPath)
    {
        using (System.IO.Stream dllStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
        {
            try
            {
                // Copy the assembly to the temporary file
                using (System.IO.Stream outFile = System.IO.File.Create(outPath))
                {
                    dllStream.CopyTo(outFile);
                }
            }
            catch
            {
            }
        }
    }

    /// <summary>
    /// Loads the correct version of p4bridge.dll, based on the bit with of the current architecture.
    /// Note that this is called by the module initializer, which gets called just after this module
    /// is loaded but before any other code inside it is executed.
    /// </summary>
    internal static void LoadP4BridgeDLL()
    {
        // Figure out where we are going to put the p4bridge.dll once we've extracted it
        string codeBase = Assembly.GetExecutingAssembly().CodeBase;
        UriBuilder uri = new UriBuilder(codeBase);
        string assemblyPath = Uri.UnescapeDataString(uri.Path);
        string assemblyDir = Path.GetDirectoryName(assemblyPath);
        string dllPath = Path.Combine(assemblyDir, "p4bridge.dll");

        // Extract the correct architecture version of p4bridge.dll from our assembly's resources
        string resourceName = Environment.Is64BitProcess ? "Perforce.P4.p4bridge64.dll" : "Perforce.P4.p4bridge86.dll";

        // If the dll already exists, then we shouldn't have to try extracting it again unless it fails to load
        if (System.IO.File.Exists(dllPath))
        {
            // Attempt to load the DLL
            if (LoadLibrary(dllPath) != IntPtr.Zero)
                return;
        }

        // DLL either wasn't already extracted, or failed to load. Try again!
        ExtractResource(resourceName, dllPath);

        // Attempt to load the DLL
        IntPtr h = LoadLibrary(dllPath);
        System.Diagnostics.Debug.Assert(h != IntPtr.Zero, "Unable to load library " + dllPath);
    }
}

And here's the command line that should be used to hook in to the .net Module Initializer. Note that the /k:MyKey.snk argument allows the assembly to be strong signed after it's been modified.

InjectModuleInitializer.exe /k:MyKey.snk /m:Perforce.P4.P4BridgeLoader::LoadP4BridgeDLL p4api.net.dll