Biohazrd icon indicating copy to clipboard operation
Biohazrd copied to clipboard

Finish support for generating bindings to static libraries

Open PathogenDavid opened this issue 2 years ago • 5 comments

I actually got most of this done for PhysX but I decided to go in a different direction (https://github.com/InfectedLibraries/InfectedPhysX/issues/4) and I still need to polish the implementation and add proper Linux support.

Here's a Windows-specific implementation:

using Biohazrd.OutputGeneration;
using Kaisa;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;

namespace Biohazrd.Utilities
{
    public sealed class StaticLibraryToDllHelper
    {
        public ImmutableList<string> GeneratedLibFiles { get; private set; } = ImmutableList<string>.Empty;
        private readonly HashSet<string> ExtraLinkerFiles = new();

        private readonly OutputSession OutputSession;

        private static ReadOnlySpan<byte> ElfFileSignature => new byte[] { 0x7F, 0x45, 0x4C, 0x46 }; // 0x7F "ELF"
        private static ReadOnlySpan<byte> WindowsArchiveSignature => new byte[] { 0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0xA };// "!<arch>\n"
        private static int LongestSignatureLength => WindowsArchiveSignature.Length;

        public StaticLibraryToDllHelper(OutputSession outputSession)
            => OutputSession = outputSession;

        public void AddExtraLinkerFile(string filePath)
            => ExtraLinkerFiles.Add(Path.GetFullPath(filePath));

        private ImmutableSortedSet<string> GetImportsFromLibrary(string filePath)
        {
            using FileStream stream = new(filePath, FileMode.Open, FileAccess.Read);

            // Determine if the library is an ELF shared library or a Windows archive file
            Span<byte> header = stackalloc byte[LongestSignatureLength];
            if (stream.Read(header) != header.Length)
            { throw new ArgumentException("The specified file is too small to be a library.", nameof(filePath)); }

            stream.Position = 0;

            if (header.StartsWith(WindowsArchiveSignature))
            {
                Archive library = new(stream);
                ImmutableSortedSet<string>.Builder results = ImmutableSortedSet.CreateBuilder<string>();

                // Enumerate all import symbols from the package
                foreach (ArchiveMember member in library.ObjectFiles)
                {
                    if (member is CoffArchiveMember coffMember)
                    {
                        foreach (CoffSymbol coffSymbol in coffMember.Symbols)
                        {
                            // 32 is function
                            if (coffSymbol.ComplexType != (CoffSymbolComplexType)32) //TODO: Why is the documentation wrong?
                            { continue; }

                            if (coffSymbol.StorageClass == CoffSymbolStorageClass.Static && coffSymbol.Value == 0)
                            { continue; }

                            if (coffSymbol.StorageClass == CoffSymbolStorageClass.WeakExternal) // Weak externals and up used on deleting destructors from other libs and cause unresolved errors if exported
                            { continue; }

                            results.Add(coffSymbol.Name);
                        }
                    }
                }

                return results.ToImmutable();
            }
            else if (header.StartsWith(ElfFileSignature))
            {
                throw new NotImplementedException("Support for ELF static libraries is not implemented."); //TODO
            }
            else
            { throw new ArgumentException("The specified file does not appear to be in a compatible format.", nameof(filePath)); }
        }

        public void AddStaticLibrary(string filePath, string outputDllName)
        {
            filePath = Path.GetFullPath(filePath);

            if (!(Path.GetDirectoryName(outputDllName) is null or ""))
            { throw new ArgumentException("The output DLL name must not include a path.", nameof(outputDllName)); }

            if (Path.GetExtension(outputDllName).Equals(".dll", StringComparison.InvariantCultureIgnoreCase))
            { outputDllName = Path.GetFileNameWithoutExtension(outputDllName); }

            ImmutableSortedSet<string> symbols = GetImportsFromLibrary(filePath);

            if (symbols.Count == 0)
            { return; }

            // Generate the linker response file
            string responseFileName = $"{outputDllName}.rsp";
            using (StreamWriter responseFile = OutputSession.Open<StreamWriter>(responseFileName))
            {
                string relativeFilePath = Path.GetRelativePath(OutputSession.BaseOutputDirectory, filePath);

                responseFile.WriteLine("/NOLOGO");
                responseFile.WriteLine("/IGNORE:4001");

                //TODO: "warning LNK4102: export of deleting destructor" (Is it not getting exported or is it just not recommended to export?)
                // -- Yeah sounds like we probably should not export.
                // https://docs.microsoft.com/en-us/cpp/error-messages/tool-errors/linker-tools-warning-lnk4102?view=msvc-160
                responseFile.WriteLine("/IGNORE:4102");

                responseFile.WriteLine("/MACHINE:X64");
                responseFile.WriteLine("/DLL");

                // Tell the linker to create the PDB
                // (If the static library has no PDB the PDB is still generated without issue, although it probably won't be very useful.)
                responseFile.WriteLine("/DEBUG");

                responseFile.WriteLine($"\"{relativeFilePath}\"");
                responseFile.WriteLine(@"/LIBPATH:""C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Tools\MSVC\14.30.30704\lib\x64\""");
                responseFile.WriteLine(@"/LIBPATH:""C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x64\""");
                responseFile.WriteLine(@"/LIBPATH:""C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\um\x64\""");
                responseFile.WriteLine("/DEFAULTLIB:libcmt.lib"); //TODO: Don't hard code this

                foreach (string inputFile in ExtraLinkerFiles)
                {
                    if (inputFile.Equals(filePath, StringComparison.InvariantCultureIgnoreCase))
                    { continue; }

                    string relativeInputFile = Path.GetRelativePath(OutputSession.BaseOutputDirectory, inputFile);
                    responseFile.WriteLine($"\"{relativeInputFile}\"");
                }

                responseFile.WriteLine($"/OUT:\"{outputDllName}.dll\"");

                foreach (string symbol in symbols)
                { responseFile.WriteLine($"/EXPORT:{symbol}"); }
            }

            // Run linker
            //TODO: Don't hard code this path
            Process linkerProcess = Process.Start(new ProcessStartInfo(@"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\link.exe", $"@{responseFileName}")
            {
                WorkingDirectory = OutputSession.BaseOutputDirectory
            })!;
            linkerProcess.WaitForExit();

            if (linkerProcess.ExitCode != 0)
            { throw new InvalidOperationException($"Linker failed to generate dll for '{filePath}': Exited with code {linkerProcess.ExitCode}."); }

            GeneratedLibFiles = GeneratedLibFiles.Add(Path.Combine(OutputSession.BaseOutputDirectory, $"{outputDllName}.lib"));
        }

        public void AddStaticLibrary(string filePath)
            => AddStaticLibrary(filePath, Path.GetFileNameWithoutExtension(filePath));
    }
}

Linux is pretty easy, you can use -Wl,--whole-archive to link a static library into a shared library and export all of its symbols:

clang -shared -L. -Wl,--whole-archive -lPhysX_static_64 -o libPhysX_64.so -Wl,--no-whole-archive

Polish that is still needed:

  • Linux support (duh)
  • Only export symbols we actually use (instead of everything -- there's lots of weird MSVC infrastructure symbols getting exported right now)
  • Resolve the question of that weird (CoffSymbolComplexType)32 thing. (At first I thought Kaisa was wrong, but it seems to match the relevant documentation.)
  • Automatically locating the MSVC toolchain (this is the main reason for not pushing.)
  • Automatically using the correct CRT library or making it configurable. (IIRC I found a way to detect the right one but not sure where I wrote it down. I think there's a special section in the .lib for it?)

PathogenDavid avatar Oct 25 '21 16:10 PathogenDavid

Consider also supporting this with Linux ELF .o and Windows COFF .obj files since it would not be substantially more complex to add them. (Especially for Linux ELF .o since you'd actually have to prevent them from working.)

Also consider completing https://github.com/PathogenDavid/Kaisa/issues/3 before doing this. (You'll probably need it for parsing COFF files separate from archives anyway.)

PathogenDavid avatar Oct 31 '21 21:10 PathogenDavid

when does it finish? , because C# NativeAOT supports the static library.

AhmedZero avatar Apr 21 '22 01:04 AhmedZero

To answer your question directly: I don't know when I'll have time to visit this properly since it's not a very high priority for me or any of my sponsors at the moment.

This issue doesn't really apply to the NativeAOT situation since NativeAOT can do true static linking. This issue primarily relates to using the linker to convert static libraries into DLLs for use with CoreCLR.

I have not had time to test NativeAOT's direct P/Invokes. I think it should probably just work if you configure NativeAOT correctly. (If you do try it definitely let me know how it goes.)

PathogenDavid avatar Apr 21 '22 12:04 PathogenDavid

I added some codes for static libraries, I know the codes are not in perfect form but it works and I can create a repo to test it with imgui.

AhmedZero avatar Apr 21 '22 16:04 AhmedZero

https://github.com/AhmedZero/Imgui_CSharp that my repo and see the action to test nativeaot,

AhmedZero avatar Apr 21 '22 23:04 AhmedZero