Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish support for generating bindings to static libraries #224

Open
PathogenDavid opened this issue Oct 25, 2021 · 5 comments
Open

Finish support for generating bindings to static libraries #224

PathogenDavid opened this issue Oct 25, 2021 · 5 comments
Labels
Area-OutputGeneration Issues concerning the process of generating output from Biohazrd Concept-CppFeatures Issues concerning unsupported C++ features Concept-CrossPlatform Issues concerning cross-platform support Language-C# Issues specific to C# Platform-Linux Issues specific to Linux Platform-Windows Issues specific to Windows

Comments

@PathogenDavid
Copy link
Member

I actually got most of this done for PhysX but I decided to go in a different direction (MochiLibraries/Mochi.PhysX#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 PathogenDavid added Concept-CppFeatures Issues concerning unsupported C++ features Area-OutputGeneration Issues concerning the process of generating output from Biohazrd Language-C# Issues specific to C# Concept-CrossPlatform Issues concerning cross-platform support Platform-Linux Issues specific to Linux Platform-Windows Issues specific to Windows labels Oct 25, 2021
@PathogenDavid PathogenDavid added this to the Upon Demonstrated Need milestone Oct 25, 2021
@PathogenDavid
Copy link
Member Author

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 PathogenDavid/Kaisa#3 before doing this. (You'll probably need it for parsing COFF files separate from archives anyway.)

@AhmedZero
Copy link

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

@PathogenDavid
Copy link
Member Author

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.)

@AhmedZero
Copy link

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-OutputGeneration Issues concerning the process of generating output from Biohazrd Concept-CppFeatures Issues concerning unsupported C++ features Concept-CrossPlatform Issues concerning cross-platform support Language-C# Issues specific to C# Platform-Linux Issues specific to Linux Platform-Windows Issues specific to Windows
Projects
None yet
Development

No branches or pull requests

2 participants