Biohazrd
Biohazrd copied to clipboard
Finish support for generating bindings to static libraries
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?)
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.)
when does it finish? , because C# NativeAOT supports the static library.
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.)
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.
https://github.com/AhmedZero/Imgui_CSharp that my repo and see the action to test nativeaot,