forked from Ivasoft/DllExports
Initial commit
This commit is contained in:
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#################
|
||||||
|
# Visual Studio #
|
||||||
|
#################
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
|
||||||
|
# NuGet Packages Directory
|
||||||
|
packages/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# MSBuild logs
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
|
||||||
|
# ReSharper settings
|
||||||
|
*.DotSettings
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Windows detritus #
|
||||||
|
####################
|
||||||
|
|
||||||
|
# Windows image file caches
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
##################
|
||||||
|
# MacOS detritus #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
.DS_Store
|
||||||
65
DllExports.MSBuild/DllExports.MSBuild.csproj
Normal file
65
DllExports.MSBuild/DllExports.MSBuild.csproj
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\Version.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net472;netstandard2.0;net5.0</TargetFrameworks>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
|
||||||
|
<Title>DllExports</Title>
|
||||||
|
<PackageId>DllExports</PackageId>
|
||||||
|
<Authors>lordmilko</Authors>
|
||||||
|
<Description>Unmanaged Exports for legacy/SDK style projects</Description>
|
||||||
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
|
<Copyright>(c) 2023 lordmilko. All rights reserved.</Copyright>
|
||||||
|
|
||||||
|
<PackageReleaseNotes>DllExports provides unmanaged exports for both legacy and SDK style projects.
|
||||||
|
|
||||||
|
Unlike other libraries that rely on a wacky series of external dependencies, DllExports has everything it needs to do its job built in.
|
||||||
|
|
||||||
|
DllExports is entirely driven by its MSBuild task, and provides a number of knobs you can adjust to customize the resulting assemblies,
|
||||||
|
including converting AnyCPU assemblies into both x86 and x64 outputs.
|
||||||
|
|
||||||
|
Note that when using IDA Pro, load the file as "Portable executable for 80386 (PE)" instead of "Microsoft.NET assembly" in order to see the exports.
|
||||||
|
|
||||||
|
In order to be able to debug your exports in Visual Studio you must be targeting .NET Framework. .NET Standard exports work but you can't debug them.
|
||||||
|
.NET Core applications can't truly have unmanaged exports as you can't use mscoree to load their runtime. Consider using a library such as DNNE for proper .NET Core support.</PackageReleaseNotes>
|
||||||
|
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\LICENSE" Pack="true" PackagePath="" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Build.Framework" Version="16.11.0" IncludeAssets="compile" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.11.0" IncludeAssets="compile" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="dnlib" Version="3.6.0" IncludeAssets="all" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="build\*">
|
||||||
|
<Pack>true</Pack>
|
||||||
|
<PackagePath>build</PackagePath>
|
||||||
|
</None>
|
||||||
|
|
||||||
|
<ProjectReference Include="..\DllExports\DllExports.csproj" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Copy dependencies to output dir -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences">
|
||||||
|
<ItemGroup>
|
||||||
|
<_ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Add file to package with consideration of sub folder. If empty, the root folder is chosen. -->
|
||||||
|
<BuildOutputInPackage Include="@(_ReferenceCopyLocalPaths)" TargetPath="%(_ReferenceCopyLocalPaths.DestinationSubDirectory)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
137
DllExports.MSBuild/GenerateDllExports.cs
Normal file
137
DllExports.MSBuild/GenerateDllExports.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
|
||||||
|
namespace DllExports.MSBuild
|
||||||
|
{
|
||||||
|
public class GenerateDllExports : Task
|
||||||
|
{
|
||||||
|
private ExportOptions options = new ExportOptions();
|
||||||
|
|
||||||
|
public bool Enabled
|
||||||
|
{
|
||||||
|
get => options.Enabled;
|
||||||
|
set => options.Enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string InputFile
|
||||||
|
{
|
||||||
|
get => options.InputFile;
|
||||||
|
set => options.InputFile = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string OutputFile
|
||||||
|
{
|
||||||
|
get => options.OutputFile;
|
||||||
|
set => options.OutputFile = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Architectures
|
||||||
|
{
|
||||||
|
get => options.Architectures;
|
||||||
|
set => options.Architectures = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ArchitectureNameFormat
|
||||||
|
{
|
||||||
|
get => options.ArchitectureNameFormat;
|
||||||
|
set => options.ArchitectureNameFormat = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveInputFile
|
||||||
|
{
|
||||||
|
get => options.RemoveInputFile;
|
||||||
|
set => options.RemoveInputFile = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
{
|
||||||
|
Log.LogMessage($"DllExports{nameof(Enabled)} was false. Nothing to do");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(InputFile))
|
||||||
|
{
|
||||||
|
Log.LogError($"DllExports{nameof(InputFile)} must be specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.LogMessage(MessageImportance.Normal, $"Processing file '{InputFile}'");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(OutputFile))
|
||||||
|
{
|
||||||
|
Log.LogError($"DllExports{nameof(OutputFile)} must be specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Architectures != null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ArchitectureNameFormat))
|
||||||
|
{
|
||||||
|
Log.LogError($"DllExports{nameof(ArchitectureNameFormat)} must be specified when DllExports{nameof(Architectures)} is specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validArchs = new[]
|
||||||
|
{
|
||||||
|
"I386",
|
||||||
|
"AMD64"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var arch in options.Architectures)
|
||||||
|
{
|
||||||
|
if (!validArchs.Contains(arch, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Log.LogError($"Invalid architecture '{arch}' specified. Valid architectures: {string.Join(", ", validArchs)}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputs = options.CalculateOutputFiles();
|
||||||
|
|
||||||
|
if (options.RemoveInputFile && outputs.Any(o => StringComparer.OrdinalIgnoreCase.Equals(o.Path, options.InputFile)))
|
||||||
|
{
|
||||||
|
Log.LogError($"Cannot set option DllExports{nameof(RemoveInputFile)} to true when inputs and outputs are the same file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskRunner = new IsolatedTaskRunner();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
taskRunner.Execute(options);
|
||||||
|
}
|
||||||
|
catch (TargetInvocationException ex)
|
||||||
|
{
|
||||||
|
Log.LogErrorFromException(ex.InnerException);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.LogErrorFromException(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.RemoveInputFile)
|
||||||
|
File.Delete(options.InputFile);
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(options.InputFile);
|
||||||
|
var dllExportsDll = Path.Combine(directory, "DllExports.dll");
|
||||||
|
|
||||||
|
if (File.Exists(dllExportsDll))
|
||||||
|
File.Delete(dllExportsDll);
|
||||||
|
|
||||||
|
Log.LogMessage(MessageImportance.High, string.Empty);
|
||||||
|
|
||||||
|
foreach (var output in outputs)
|
||||||
|
Log.LogMessage(MessageImportance.High, $"DllExports ({output.Name}) -> {output.Path}");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
DllExports.MSBuild/IsolatedTaskRunner.cs
Normal file
64
DllExports.MSBuild/IsolatedTaskRunner.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
#if NET
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace DllExports.MSBuild
|
||||||
|
{
|
||||||
|
class IsolatedTaskRunner
|
||||||
|
{
|
||||||
|
public void Execute(ExportOptions options)
|
||||||
|
{
|
||||||
|
#if NET
|
||||||
|
var context = new NetCoreAssemblyLoadContext();
|
||||||
|
#else
|
||||||
|
var context = new NetFrameworkAssemblyLoadContext();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assemblyPath = GetType().Assembly.Location;
|
||||||
|
var assemblyDirectory = Path.GetDirectoryName(assemblyPath);
|
||||||
|
var dllExportsPath = Path.Combine(assemblyDirectory, "DllExports.dll");
|
||||||
|
|
||||||
|
if (!File.Exists(dllExportsPath))
|
||||||
|
throw new FileNotFoundException($"Could not find '{dllExportsPath}'", dllExportsPath);
|
||||||
|
|
||||||
|
context.SetDllDirectory(assemblyDirectory);
|
||||||
|
|
||||||
|
using (var fileStream = File.OpenRead(dllExportsPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = context.LoadAssembly(fileStream);
|
||||||
|
|
||||||
|
context.Export(assembly, options);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
//I'm not sure if its such a great idea to be closing the file AFTER we've already unloaded,
|
||||||
|
//so we kick off an unload here
|
||||||
|
context.Unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
//If we crashed before we attempted to unload above, we need to unload here
|
||||||
|
context.Unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAssemblyLoadContext
|
||||||
|
{
|
||||||
|
Assembly LoadAssembly(Stream stream);
|
||||||
|
|
||||||
|
void SetDllDirectory(string path);
|
||||||
|
|
||||||
|
void Export(Assembly assembly, ExportOptions options);
|
||||||
|
|
||||||
|
void Unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
71
DllExports.MSBuild/NetCoreAssemblyLoadContext.cs
Normal file
71
DllExports.MSBuild/NetCoreAssemblyLoadContext.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#if NET
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DllExports.MSBuild
|
||||||
|
{
|
||||||
|
class AssemblyContext : AssemblyLoadContext
|
||||||
|
{
|
||||||
|
public AssemblyContext() : base(true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetCoreAssemblyLoadContext : IAssemblyLoadContext
|
||||||
|
{
|
||||||
|
private AssemblyContext loader = new AssemblyContext();
|
||||||
|
private bool unloaded;
|
||||||
|
|
||||||
|
public NetCoreAssemblyLoadContext()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Assembly LoadAssembly(Stream stream) =>
|
||||||
|
loader.LoadFromStream(stream);
|
||||||
|
|
||||||
|
public void SetDllDirectory(string path)
|
||||||
|
{
|
||||||
|
var files = Directory.EnumerateFiles(path, "*.dll").ToArray();
|
||||||
|
|
||||||
|
loader.Resolving += (ctx, name) =>
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (StringComparer.OrdinalIgnoreCase.Equals(Path.GetFileNameWithoutExtension(file), name.Name))
|
||||||
|
return ctx.LoadFromAssemblyPath(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Export(Assembly assembly, ExportOptions options)
|
||||||
|
{
|
||||||
|
var exporterType = assembly.GetType("DllExports.Exporter");
|
||||||
|
var exportMethod = exporterType.GetMethod("Export");
|
||||||
|
|
||||||
|
exportMethod.Invoke(null, new object[]
|
||||||
|
{
|
||||||
|
options.Enabled,
|
||||||
|
options.InputFile,
|
||||||
|
options.OutputFile,
|
||||||
|
options.Architectures,
|
||||||
|
options.ArchitectureNameFormat,
|
||||||
|
options.RemoveInputFile
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unload()
|
||||||
|
{
|
||||||
|
if (unloaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loader.Unload();
|
||||||
|
unloaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
81
DllExports.MSBuild/NetFrameworkAssemblyLoadContext.cs
Normal file
81
DllExports.MSBuild/NetFrameworkAssemblyLoadContext.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#if !NET
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DllExports.MSBuild
|
||||||
|
{
|
||||||
|
internal class NetFrameworkAssemblyLoadContext : MarshalByRefObject, IAssemblyLoadContext
|
||||||
|
{
|
||||||
|
private AppDomain appDomain;
|
||||||
|
#pragma warning disable 649
|
||||||
|
private object helper;
|
||||||
|
#pragma warning restore 649
|
||||||
|
private bool unloaded;
|
||||||
|
|
||||||
|
public NetFrameworkAssemblyLoadContext()
|
||||||
|
{
|
||||||
|
appDomain = AppDomain.CreateDomain("DllExportAppDomain");
|
||||||
|
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
helper = appDomain.CreateInstanceFromAndUnwrap(
|
||||||
|
GetType().Assembly.Location,
|
||||||
|
typeof(RemoteHelper).FullName,
|
||||||
|
false,
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
|
null,
|
||||||
|
new object[0],
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
#else
|
||||||
|
throw new NotSupportedException();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public Assembly LoadAssembly(Stream stream)
|
||||||
|
{
|
||||||
|
//Attempting to load DllExports.dll inside of MSBuild will cause it to try and look the DLL up
|
||||||
|
//in the Visual Studio installation directory. Our assembly resolver is never called. As such,
|
||||||
|
//we don't even bother trying to pre-emptively load DllExports.dll. We don't need to anyway,
|
||||||
|
//since it will "just work" when we dispatch to it via RemoteHelper
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDllDirectory(string path)
|
||||||
|
{
|
||||||
|
//I don't know why MSBuild gets upset when I try and cast my transparent proxy to RemoteHelper,
|
||||||
|
//and I don't really care. So we'll use type object instead and abuse the Equals method to dispatch
|
||||||
|
//everything
|
||||||
|
helper.Equals(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Export(Assembly assembly, ExportOptions options)
|
||||||
|
{
|
||||||
|
//I don't know why MSBuild gets upset when I try and cast my transparent proxy to RemoteHelper,
|
||||||
|
//and I don't really care. So we'll use type object instead and abuse the Equals method to dispatch
|
||||||
|
//everything
|
||||||
|
helper.Equals(
|
||||||
|
new object[]
|
||||||
|
{
|
||||||
|
options.Enabled,
|
||||||
|
options.InputFile,
|
||||||
|
options.OutputFile,
|
||||||
|
options.Architectures,
|
||||||
|
options.ArchitectureNameFormat,
|
||||||
|
options.RemoveInputFile
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unload()
|
||||||
|
{
|
||||||
|
if (unloaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AppDomain.Unload(appDomain);
|
||||||
|
unloaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
66
DllExports.MSBuild/RemoteHelper.cs
Normal file
66
DllExports.MSBuild/RemoteHelper.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#if NETFRAMEWORK
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DllExports.MSBuild
|
||||||
|
{
|
||||||
|
public class RemoteHelper : MarshalByRefObject
|
||||||
|
{
|
||||||
|
private string[] files;
|
||||||
|
|
||||||
|
private Assembly OnAssemblyResolve(object sender, ResolveEventArgs e)
|
||||||
|
{
|
||||||
|
var name = e.Name;
|
||||||
|
|
||||||
|
if (e.Name != null && e.Name.Contains(","))
|
||||||
|
name = name.Substring(0, name.IndexOf(','));
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (StringComparer.OrdinalIgnoreCase.Equals(Path.GetFileNameWithoutExtension(file), name))
|
||||||
|
return Assembly.LoadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is string)
|
||||||
|
{
|
||||||
|
files = Directory.EnumerateFiles(obj.ToString(), "*.dll").ToArray();
|
||||||
|
|
||||||
|
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//For some reason when we use reflection to invoke the method in the remote AppDomain we're still ending up back
|
||||||
|
//in our original domain; as such, we'll force ourselves to run in another domain by creating an object (RemoteHelper)
|
||||||
|
//in the remote AppDomain and doing everything through here
|
||||||
|
|
||||||
|
var arr = (object[])obj;
|
||||||
|
|
||||||
|
Exporter.Export(
|
||||||
|
(bool) arr[0],
|
||||||
|
(string) arr[1],
|
||||||
|
(string) arr[2],
|
||||||
|
(string[]) arr[3],
|
||||||
|
(string) arr[4],
|
||||||
|
(bool) arr[5]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return base.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object InitializeLifetimeService() => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
11
DllExports.MSBuild/build/DllExports.props
Normal file
11
DllExports.MSBuild/build/DllExports.props
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- The "TaskFolder" and "TaskAssembly" properties we're defining here are just for us internally -->
|
||||||
|
<Project TreatAsLocalProperty="TaskFolder;TaskAssembly">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TaskFolder Condition="'$(MSBuildRuntimeType)' == 'Core'">net5.0</TaskFolder>
|
||||||
|
<TaskFolder Condition="'$(MSBuildRuntimeType)' != 'Core'">net472</TaskFolder>
|
||||||
|
|
||||||
|
<TaskAssembly Condition="'$(TaskAssembly)' == ''">$(MSBuildThisFileDirectory)..\tasks\$(TaskFolder)\DllExports.MSBuild.dll</TaskAssembly>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<UsingTask TaskName="DllExports.MSBuild.GenerateDllExports" AssemblyFile="$(TaskAssembly)" />
|
||||||
|
</Project>
|
||||||
36
DllExports.MSBuild/build/DllExports.targets
Normal file
36
DllExports.MSBuild/build/DllExports.targets
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<Project>
|
||||||
|
<Target Name="DllExportGenerateExports" AfterTargets="Build">
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Whether to process DLL Exports -->
|
||||||
|
<DllExportsEnabled Condition="'$(DllExportsEnabled)' == ''">true</DllExportsEnabled>
|
||||||
|
|
||||||
|
<!-- The full path to the file to generate exports for -->
|
||||||
|
<DllExportsInputFile Condition="'$(DllExportsInputFile)' == ''">$(TargetPath)</DllExportsInputFile>
|
||||||
|
|
||||||
|
<!-- The full path to the file the modified assembly should be saved as. Can be the same as the input file.
|
||||||
|
If this value does not contain a path to a file, the same directory as DllExportsInputFile will be used -->
|
||||||
|
<DllExportsOutputFile Condition="'$(DllExportsOutputFile)' == ''">$(DllExportsInputFile)</DllExportsOutputFile>
|
||||||
|
|
||||||
|
<!-- CPU architectures to generate separate architecture files for. Valid values: i386, AMD64 -->
|
||||||
|
<DllExportsArchitectures Condition="'$(DllExportsArchitectures)' == ''"></DllExportsArchitectures>
|
||||||
|
|
||||||
|
<!-- When DllExportsArchitectures is specified, what should the filename format of each file be, e.g. Foo.x64.
|
||||||
|
Value of {name} is derived from file name specified in DllExportsOutputFile.
|
||||||
|
i386 will be called "x86", and "AMD64" will be called x64 -->
|
||||||
|
<DllExportsArchitectureNameFormat Condition="'$(DllExportsArchitectureNameFormat)' == ''">{name}.{arch}</DllExportsArchitectureNameFormat>
|
||||||
|
|
||||||
|
<!-- Whether to remove DllExportsInputFile after generating exports. Useful when you want to generate per-architecture exports.
|
||||||
|
Only valid when DllExportsInputFile and DllExportsOutputFile are different or you're doing per-architecture exports -->
|
||||||
|
<DllExportsRemoveInputFile Condition="'$(DllExportsRemoveInputFile)' == ''">false</DllExportsRemoveInputFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<GenerateDllExports
|
||||||
|
Enabled="$(DllExportsEnabled)"
|
||||||
|
InputFile="$(DllExportsInputFile)"
|
||||||
|
OutputFile="$(DllExportsOutputFile)"
|
||||||
|
Architectures="$(DllExportsArchitectures)"
|
||||||
|
ArchitectureNameFormat="$(DllExportsArchitectureNameFormat)"
|
||||||
|
RemoveInputFile="$(DllExportsRemoveInputFile)"
|
||||||
|
/>
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
27
DllExports.Tests/AssertEx.cs
Normal file
27
DllExports.Tests/AssertEx.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace DllExports.Tests
|
||||||
|
{
|
||||||
|
public static class AssertEx
|
||||||
|
{
|
||||||
|
public static void Throws<T>(Action action, string message, bool checkMessage = true) where T : Exception
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
|
||||||
|
Assert.Fail($"Expected an assertion of type {typeof(T)} to be thrown, however no exception occurred");
|
||||||
|
}
|
||||||
|
catch (T ex)
|
||||||
|
{
|
||||||
|
if (checkMessage)
|
||||||
|
Assert.IsTrue(ex.Message.Contains(message), $"Exception message '{ex.Message}' did not contain string '{message}'");
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!(ex is AssertFailedException))
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
DllExports.Tests/DllExports.Tests.csproj
Normal file
23
DllExports.Tests/DllExports.Tests.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net5.0;net472</TargetFrameworks>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.0.2" />
|
||||||
|
|
||||||
|
<PackageReference Include="Microsoft.Build.Framework" Version="16.11.0" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.11.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\DllExports.MSBuild\DllExports.MSBuild.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
158
DllExports.Tests/ExporterTests.cs
Normal file
158
DllExports.Tests/ExporterTests.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using DllExports.MSBuild;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace DllExports.Tests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class ExporterTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_Success()
|
||||||
|
{
|
||||||
|
TestExporter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_Enabled_False()
|
||||||
|
{
|
||||||
|
TestExporter(t => t.Enabled = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_Architectures_DefaultNameFormat()
|
||||||
|
{
|
||||||
|
TestExporter(t =>
|
||||||
|
{
|
||||||
|
t.Architectures = new[] {"i386"};
|
||||||
|
t.ArchitectureNameFormat = "{name}.{arch}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_Architectures_CustomNameFormat()
|
||||||
|
{
|
||||||
|
TestExporter(t =>
|
||||||
|
{
|
||||||
|
t.Architectures = new[] { "i386" };
|
||||||
|
t.ArchitectureNameFormat = "{name}.foo.{arch}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_InputFile_Empty_Throws()
|
||||||
|
{
|
||||||
|
var task = new GenerateDllExports
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
InputFile = null
|
||||||
|
};
|
||||||
|
|
||||||
|
AssertEx.Throws<InvalidOperationException>(
|
||||||
|
() => task.Execute(),
|
||||||
|
"DllExportsInputFile must be specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_OutputFile_Empty_Throws()
|
||||||
|
{
|
||||||
|
var task = new GenerateDllExports
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
InputFile = "foo"
|
||||||
|
};
|
||||||
|
|
||||||
|
task.BuildEngine = new MockBuildEngine();
|
||||||
|
|
||||||
|
AssertEx.Throws<InvalidOperationException>(
|
||||||
|
() => task.Execute(),
|
||||||
|
"DllExportsOutputFile must be specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_ArchitectureNameFormat_WithArchitectures_Empty_Throws()
|
||||||
|
{
|
||||||
|
AssertEx.Throws<InvalidOperationException>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
TestExporter(t =>
|
||||||
|
{
|
||||||
|
t.Architectures = new[] { "i386" };
|
||||||
|
t.ArchitectureNameFormat = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"DllExportsArchitectureNameFormat must be specified when DllExportsArchitectures is specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_RemoveInputFile_SameInputAndOutput_Throws()
|
||||||
|
{
|
||||||
|
AssertEx.Throws<InvalidOperationException>(
|
||||||
|
() => TestExporter(t => t.RemoveInputFile = true),
|
||||||
|
"Cannot set option DllExportsRemoveInputFile to true when inputs and outputs are the same file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Exporter_RemoveInputFile_ArchitectureSpecificExports()
|
||||||
|
{
|
||||||
|
TestExporter(t =>
|
||||||
|
{
|
||||||
|
t.Architectures = new[] { "i386" };
|
||||||
|
t.ArchitectureNameFormat = "{name}.{arch}";
|
||||||
|
t.RemoveInputFile = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TestExporter(Action<GenerateDllExports> configure)
|
||||||
|
{
|
||||||
|
var assemblyPath = GetType().Assembly.Location;
|
||||||
|
var directory = Path.GetDirectoryName(assemblyPath);
|
||||||
|
|
||||||
|
var targetFramework = "netstandard2.0";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
var configuration = "Debug";
|
||||||
|
#else
|
||||||
|
var configuration = "Release";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var solution = Path.GetFullPath(Path.Combine(directory, "..", "..", "..", ".."));
|
||||||
|
|
||||||
|
var testFile = Path.Combine(solution, "Samples", "SingleTarget", "bin", configuration, targetFramework, "SingleTarget.dll");
|
||||||
|
|
||||||
|
if (!File.Exists(testFile))
|
||||||
|
throw new InvalidOperationException($"Test File '{testFile}' was not found. Test project must be compiled prior to running tests");
|
||||||
|
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
tmpFile = Path.ChangeExtension(tmpFile, ".dll");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(testFile, tmpFile, true);
|
||||||
|
|
||||||
|
var task = new GenerateDllExports
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
InputFile = tmpFile,
|
||||||
|
OutputFile = tmpFile,
|
||||||
|
|
||||||
|
BuildEngine = new MockBuildEngine()
|
||||||
|
};
|
||||||
|
|
||||||
|
configure?.Invoke(task);
|
||||||
|
|
||||||
|
task.Execute();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(tmpFile))
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
DllExports.Tests/MockBuildEngine.cs
Normal file
37
DllExports.Tests/MockBuildEngine.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
|
||||||
|
namespace DllExports.Tests
|
||||||
|
{
|
||||||
|
class MockBuildEngine : IBuildEngine
|
||||||
|
{
|
||||||
|
public void LogErrorEvent(BuildErrorEventArgs e)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogWarningEvent(BuildWarningEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogMessageEvent(BuildMessageEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogCustomEvent(CustomBuildEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
|
||||||
|
IDictionary targetOutputs)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContinueOnError { get; }
|
||||||
|
public int LineNumberOfTaskNode { get; }
|
||||||
|
public int ColumnNumberOfTaskNode { get; }
|
||||||
|
public string ProjectFileOfTaskNode { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
DllExports.sln
Normal file
37
DllExports.sln
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 16
|
||||||
|
VisualStudioVersion = 16.0.32126.315
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllExports", "DllExports\DllExports.csproj", "{ABBFD833-DD5F-40EE-B2EA-9A8E9641283A}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllExports.MSBuild", "DllExports.MSBuild\DllExports.MSBuild.csproj", "{0F2800DF-65E6-47F8-949F-E401881D5970}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllExports.Tests", "DllExports.Tests\DllExports.Tests.csproj", "{D8920F8A-90C4-4D1E-9750-8ED1D79086C9}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{ABBFD833-DD5F-40EE-B2EA-9A8E9641283A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{ABBFD833-DD5F-40EE-B2EA-9A8E9641283A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ABBFD833-DD5F-40EE-B2EA-9A8E9641283A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{ABBFD833-DD5F-40EE-B2EA-9A8E9641283A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0F2800DF-65E6-47F8-949F-E401881D5970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0F2800DF-65E6-47F8-949F-E401881D5970}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0F2800DF-65E6-47F8-949F-E401881D5970}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0F2800DF-65E6-47F8-949F-E401881D5970}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D8920F8A-90C4-4D1E-9750-8ED1D79086C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D8920F8A-90C4-4D1E-9750-8ED1D79086C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D8920F8A-90C4-4D1E-9750-8ED1D79086C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D8920F8A-90C4-4D1E-9750-8ED1D79086C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {787DE98D-5953-4B1F-9291-5EFDD6E38922}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
30
DllExports/DllExportAttribute.cs
Normal file
30
DllExports/DllExportAttribute.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace DllExports
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class DllExportAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string ExportName { get; }
|
||||||
|
|
||||||
|
public CallingConvention CallingConvention { get; }
|
||||||
|
|
||||||
|
public DllExportAttribute()
|
||||||
|
{
|
||||||
|
CallingConvention = CallingConvention.StdCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DllExportAttribute(string exportName)
|
||||||
|
{
|
||||||
|
ExportName = exportName;
|
||||||
|
CallingConvention = CallingConvention.StdCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DllExportAttribute(string exportName, CallingConvention callingConvention)
|
||||||
|
{
|
||||||
|
ExportName = exportName;
|
||||||
|
CallingConvention = callingConvention;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
DllExports/DllExports.csproj
Normal file
17
DllExports/DllExports.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\Version.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
|
||||||
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
|
||||||
|
<PackageId>DllExports.Internal</PackageId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="dnlib" Version="3.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
85
DllExports/ExportOptions.cs
Normal file
85
DllExports/ExportOptions.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace DllExports
|
||||||
|
{
|
||||||
|
internal class ExportOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
public string InputFile { get; set; }
|
||||||
|
|
||||||
|
public string OutputFile { get; set; }
|
||||||
|
|
||||||
|
public string[] Architectures { get; set; }
|
||||||
|
|
||||||
|
public string ArchitectureNameFormat { get; set; }
|
||||||
|
|
||||||
|
public bool RemoveInputFile { get; set; }
|
||||||
|
|
||||||
|
internal (string Path, string Name, bool? Is32Bit)[] CalculateOutputFiles()
|
||||||
|
{
|
||||||
|
var outputFile = GetOutputFileName();
|
||||||
|
|
||||||
|
if (Architectures == null || Architectures.Length == 0)
|
||||||
|
return new (string Path, string Name, bool? Is32Bit)[] { (outputFile, "Default", null) };
|
||||||
|
|
||||||
|
var results = new List<(string Path, string Name, bool? Is32Bit)>();
|
||||||
|
|
||||||
|
foreach (var arch in Architectures)
|
||||||
|
{
|
||||||
|
var outputDir = Path.GetDirectoryName(outputFile);
|
||||||
|
var baseName = Path.GetFileNameWithoutExtension(outputFile);
|
||||||
|
var ext = Path.GetExtension(outputFile);
|
||||||
|
|
||||||
|
string displayArch;
|
||||||
|
bool is32Bit;
|
||||||
|
|
||||||
|
switch (arch.ToLower())
|
||||||
|
{
|
||||||
|
case "i386":
|
||||||
|
displayArch = "x86";
|
||||||
|
is32Bit = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "amd64":
|
||||||
|
displayArch = "x64";
|
||||||
|
is32Bit = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Architecture '{arch}' is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
var newName = Path.Combine(
|
||||||
|
outputDir,
|
||||||
|
ArchitectureNameFormat.Replace("{name}", baseName).Replace("{arch}", displayArch) + ext
|
||||||
|
);
|
||||||
|
|
||||||
|
results.Add((newName, displayArch, is32Bit));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetOutputFileName()
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(OutputFile);
|
||||||
|
|
||||||
|
var outputFile = OutputFile;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
directory = Path.GetDirectoryName(InputFile);
|
||||||
|
var fileName = Path.GetFileName(OutputFile);
|
||||||
|
outputFile = Path.Combine(directory, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Path.GetExtension(outputFile)))
|
||||||
|
outputFile += Path.GetExtension(InputFile);
|
||||||
|
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
175
DllExports/Exporter.cs
Normal file
175
DllExports/Exporter.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using dnlib.DotNet;
|
||||||
|
using dnlib.DotNet.MD;
|
||||||
|
using dnlib.DotNet.Writer;
|
||||||
|
using dnlib.PE;
|
||||||
|
using CallingConvention = System.Runtime.InteropServices.CallingConvention;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("DllExports.MSBuild")]
|
||||||
|
[assembly: InternalsVisibleTo("DllExports.Tests")]
|
||||||
|
|
||||||
|
namespace DllExports
|
||||||
|
{
|
||||||
|
internal class Exporter
|
||||||
|
{
|
||||||
|
public static void Export(
|
||||||
|
bool enabled,
|
||||||
|
string inputFile,
|
||||||
|
string outputFile,
|
||||||
|
string[] architectures,
|
||||||
|
string architectureNameFormat,
|
||||||
|
bool removeInputFile)
|
||||||
|
{
|
||||||
|
var options = new ExportOptions
|
||||||
|
{
|
||||||
|
Enabled = enabled,
|
||||||
|
InputFile = inputFile,
|
||||||
|
OutputFile = outputFile,
|
||||||
|
Architectures = architectures,
|
||||||
|
ArchitectureNameFormat = architectureNameFormat,
|
||||||
|
RemoveInputFile = removeInputFile
|
||||||
|
};
|
||||||
|
|
||||||
|
var moduleContext = ModuleDef.CreateModuleContext();
|
||||||
|
|
||||||
|
//If we simply create a ModuleDefMD from a memory stream, we're not going to know our original filename
|
||||||
|
//and be able to find our PDB. If we simply load the module from the filename, our file will be in use
|
||||||
|
//if we attempt to overwrite it. As such, we tap into the underlying PEImage type, which lets us read the
|
||||||
|
//entire byte array of the file AND specify the input filename
|
||||||
|
var bytes = File.ReadAllBytes(options.InputFile);
|
||||||
|
var peImage = new PEImage(bytes, options.InputFile);
|
||||||
|
|
||||||
|
var module = ModuleDefMD.Load(peImage, moduleContext);
|
||||||
|
|
||||||
|
var exportedMethods = module.Types.SelectMany(t => t.Methods)
|
||||||
|
.Where(m => m.IsStatic)
|
||||||
|
.Select(m => new
|
||||||
|
{
|
||||||
|
Method = m,
|
||||||
|
Attrib = m.CustomAttributes.Find(typeof(DllExportAttribute).FullName)
|
||||||
|
})
|
||||||
|
.Where(v => v.Attrib != null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var item in exportedMethods)
|
||||||
|
{
|
||||||
|
var exportedMethod = item.Method;
|
||||||
|
|
||||||
|
string exportName;
|
||||||
|
string callingConvention = null;
|
||||||
|
|
||||||
|
if (item.Attrib.ConstructorArguments.Count == 0)
|
||||||
|
exportName = exportedMethod.Name;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (item.Attrib.ConstructorArguments.Count == 2)
|
||||||
|
{
|
||||||
|
var conv = (CallingConvention)item.Attrib.ConstructorArguments[1].Value;
|
||||||
|
|
||||||
|
switch (conv)
|
||||||
|
{
|
||||||
|
case CallingConvention.StdCall:
|
||||||
|
callingConvention = typeof(CallConvStdcall).Name;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CallingConvention.Cdecl:
|
||||||
|
callingConvention = typeof(CallConvCdecl).Name;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CallingConvention.FastCall:
|
||||||
|
callingConvention = typeof(CallConvFastcall).Name;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CallingConvention.ThisCall:
|
||||||
|
callingConvention = typeof(CallConvThiscall).Name;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CallingConvention.Winapi:
|
||||||
|
callingConvention = typeof(CallConvStdcall).Name;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Calling convention {conv} is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportName = item.Attrib.ConstructorArguments[0].Value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callingConvention == null)
|
||||||
|
callingConvention = typeof(CallConvStdcall).Name;
|
||||||
|
|
||||||
|
exportedMethod.ExportInfo = new MethodExportInfo(exportName);
|
||||||
|
exportedMethod.IsUnmanagedExport = true;
|
||||||
|
|
||||||
|
exportedMethod.MethodSig.RetType = new CModOptSig(
|
||||||
|
module.CorLibTypes.GetTypeRef("System.Runtime.CompilerServices", callingConvention),
|
||||||
|
exportedMethod.MethodSig.RetType
|
||||||
|
);
|
||||||
|
|
||||||
|
exportedMethod.CustomAttributes.Remove(item.Attrib);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.IsILOnly = false;
|
||||||
|
|
||||||
|
ClearEditAndContinue(module);
|
||||||
|
|
||||||
|
var outputFiles = options.CalculateOutputFiles();
|
||||||
|
|
||||||
|
foreach (var output in outputFiles)
|
||||||
|
{
|
||||||
|
var moduleOptions = GetModuleOptions(module, output.Is32Bit);
|
||||||
|
|
||||||
|
var dir = Path.GetDirectoryName(output.Path);
|
||||||
|
|
||||||
|
if (!Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
moduleOptions.WritePdb = true;
|
||||||
|
|
||||||
|
module.Write(options.OutputFile, moduleOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModuleWriterOptions GetModuleOptions(ModuleDefMD module, bool? is32Bit)
|
||||||
|
{
|
||||||
|
var moduleOptions = new ModuleWriterOptions(module);
|
||||||
|
|
||||||
|
moduleOptions.Cor20HeaderOptions.Flags &= ~(ComImageFlags.ILOnly);
|
||||||
|
|
||||||
|
if (is32Bit != null)
|
||||||
|
{
|
||||||
|
moduleOptions.PEHeadersOptions.Machine = is32Bit.Value ? Machine.I386 : Machine.AMD64;
|
||||||
|
|
||||||
|
if (is32Bit.Value)
|
||||||
|
{
|
||||||
|
moduleOptions.Cor20HeaderOptions.Flags |= ComImageFlags.Bit32Required;
|
||||||
|
moduleOptions.Cor20HeaderOptions.Flags &= ~(ComImageFlags.Bit32Preferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moduleOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearEditAndContinue(ModuleDefMD module)
|
||||||
|
{
|
||||||
|
var debuggableAttrib = module.Assembly.CustomAttributes.Find("System.Diagnostics.DebuggableAttribute");
|
||||||
|
|
||||||
|
if (debuggableAttrib != null && debuggableAttrib.ConstructorArguments.Count == 1)
|
||||||
|
{
|
||||||
|
var arg = debuggableAttrib.ConstructorArguments[0];
|
||||||
|
|
||||||
|
// VS' debugger crashes if value == 0x107, so clear EnC bit
|
||||||
|
if (arg.Type.FullName == "System.Diagnostics.DebuggableAttribute/DebuggingModes" && arg.Value is int value && value == 0x107)
|
||||||
|
{
|
||||||
|
arg.Value = value & ~(int)DebuggableAttribute.DebuggingModes.EnableEditAndContinue;
|
||||||
|
debuggableAttrib.ConstructorArguments[0] = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 lordmilko
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# DllExports
|
||||||
|
|
||||||
|
[](https://ci.appveyor.com/project/lordmilko/dllexports)
|
||||||
|
[](https://www.nuget.org/packages/DllExports/)
|
||||||
|
|
||||||
|
*All I want in life is a package to enable unmanaged exports via a simple MSBuild task in my .NET application*
|
||||||
|
|
||||||
|
There are several packages out there that attempt to facilitate enabling unmanaged exports in .NET applications. A big challenge with existing solutions however is that either:
|
||||||
|
|
||||||
|
* They don't work
|
||||||
|
* They require a bunch of wacky external dependencies
|
||||||
|
* They won't work because they require a bunch of wacky external dependencies
|
||||||
|
|
||||||
|
DllExports aims to be a simple .NET package that:
|
||||||
|
|
||||||
|
1. Works
|
||||||
|
2. Doesn't require any external dependencies
|
||||||
|
|
||||||
|
Thus hopefully ensuring it continues to work into the future.
|
||||||
|
|
||||||
|
**In order to be able to debug your exports in Visual Studio you must be targeting .NET Framework. .NET Standard exports work but you can't debug them
|
||||||
|
(presumably because it expected the .NET Core runtime to be loaded but .NET Framework was loaded instead). .NET Core applications can't truly have unmanaged
|
||||||
|
exports as you can't use mscoree to load their runtime. Consider using a library such as [DNNE](https://github.com/AaronRobinsonMSFT/DNNE) for proper .NET Core support
|
||||||
|
(however this will require C++ tooling to be properly installed).**
|
||||||
|
|
||||||
|
Note that when using IDA Pro, load the file as `Portable executable for 80386 (PE)` instead of `Microsoft.NET assembly` in order to see the exports.
|
||||||
|
|
||||||
|
See [Tips](#tips) for some important gotchas to be aware of.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To declare an unmanaged export, simply decorate a static method with `DllExportAttribute`
|
||||||
|
|
||||||
|
```c#
|
||||||
|
using DllExports;
|
||||||
|
|
||||||
|
[DllExport]
|
||||||
|
public static void Foo()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
You may optionally also specify the name and calling convention to use for the unmanaged export. If no calling convention is specified, by default `stdcall` will be used.
|
||||||
|
|
||||||
|
DllExports provides a number of knobs you can use to adjust how your input file will be processed.
|
||||||
|
|
||||||
|
| Property | Default Value | Description |
|
||||||
|
| -------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
|
||||||
|
| DllExportsEnabled | `true` | Whether DllExports should process unmanaged exports upon building |
|
||||||
|
| DllExportsInputFile | `$(TargetPath)` | The file DllExports should process unmanaged exports for |
|
||||||
|
| DllExportsOutputFile | `$(DllExportsInputFile)` | The file to save the modified file as |
|
||||||
|
| DllExportsArchitectures | | Architectures to generate DllExports for. e.g. `i386;AMD64`. Each architecture will get its own file. If no architecture is specified, the architecture is not modified |
|
||||||
|
| DllExportsArchitectureNameFormat | `{name}.{arch}` | The name format used when processing `DllExportsArchitectures`. Resulting filename will be `DllExportsOutputFile` directory + `DllExportsArchitectureNameFormat` + file extension |
|
||||||
|
| DllExportsRemoveInputFile | `false` | Whether to remove `DllExportsInputFile` upon generating unmanaged exports |
|
||||||
|
|
||||||
|
DllExports currently only supports generating unmanaged exports for i386 and AMD64.
|
||||||
|
|
||||||
|
When you compile a library as AnyCPU, implicitly it is actually either i386 or AMD64, and will only be loaded properly in an application with a matching architecture.
|
||||||
|
|
||||||
|
## Legacy Projects
|
||||||
|
|
||||||
|
Historically, packages have installed MSBuild props and targets via a PowerShell init script embedded within the NuGet package. Such scripts do not work, and are
|
||||||
|
essentially unnecessary when it comes to SDK style projects. As SDK style projects are a lot easier to manage than legacy style projects, it makes sense to use
|
||||||
|
SDK style projects even if you are building applications against .NET Framework. As such, DllExports does not provide a script for automatically updating your project file
|
||||||
|
for seamless support with legacy style projects. You can easily inject the required changes yourself however, as follows:
|
||||||
|
|
||||||
|
1. Insert the `props` import after all other props imports at the top of the file
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Import Project="..\..\packages\DllExports.0.1.0\build\DllExports.props" Condition="Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" />
|
||||||
|
```
|
||||||
|
2. Add a package reference, with `Private = False` so DllExports does not get emitted to your output directory
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Reference Include="DllExports">
|
||||||
|
<HintPath>..\..\packages\DllExports.0.1.0\lib\netstandard2.0\DllExports.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
```
|
||||||
|
3. Import the `targets` at the end of the file and add a `Target` to warn when NuGet packages have not been restored
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Import Project="..\..\packages\DllExports.0.1.0\build\DllExports.targets" Condition="Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\DllExports.0.1.0\build\DllExports.targets'))" />
|
||||||
|
</Target>
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the version number in the above snippets as necessary. The [NetFramework](https://github.com/lordmilko/DllExports/blob/master/Samples/NetFramework/NetFramework.csproj) sample demonstrates how your file should look like.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
* Don't use types types external to your assembly or the CLR in the method signature of your exports. e.g. do not use the `HRESULT` type from [ClrDebug](https://github.com/lordmilko/ClrDebug). The runtime is not in a position to load
|
||||||
|
external assemblies when your export is called. You can however use types defined in the same assembly that your export is defined in
|
||||||
|
* Once an external assembly has been loaded, it is safe to use types in external external assemblies in subsequently called exports
|
||||||
|
* You can force architecture specific files to be placed in an architecture specific subdirectory by setting `DllExportsArchitectureNameFormat` to something like `{arch}\{name}.{arch}` i.e. `Foo.dll` compiled for AMD64 will go to `x64\Foo.x64.dll`
|
||||||
|
* When multi-targeting, you can conditionally generate unmanaged exports for compatible assemblies as follows
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<TargetFrameworks>netstandard2.0;net472</TargetFrameworks>
|
||||||
|
|
||||||
|
<DllExportsEnabled>false</DllExportsEnabled>
|
||||||
|
<DllExportsEnabled Condition="'$(TargetFramework)' == 'net472'">true</DllExportsEnabled>
|
||||||
|
```
|
||||||
|
* When consuming third party libraries in your unmanaged export, watch out for assembly resolution issues! If you exported assembly is loaded into some other application,
|
||||||
|
when you attempt to reference a type in a third party library, the CLR is going to look in the directory of *that application* - **not** the directory that your assembly and all
|
||||||
|
its dependencies are in. Consider setting `AppDomain.CurrentDomain.AssemblyResolve` and/or pre-emptively loading your assemblies in the first export accessed via `Assembly.LoadFrom`
|
||||||
|
* The CLR will only attempt to load an assembly when a type within it is referenced within a given method. If your program relies on an outer method doing assembly resolution prior
|
||||||
|
to calling an inner method, consider decorating the inner method with `[MethodImpl(MethodImplOptions.NoInlining)]`
|
||||||
14
Samples/MultiTarget/Class1.cs
Normal file
14
Samples/MultiTarget/Class1.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using DllExports;
|
||||||
|
|
||||||
|
namespace MultiTarget
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
[DllExport("MyExport")]
|
||||||
|
public static void InternalName()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Samples/MultiTarget/MultiTarget.csproj
Normal file
13
Samples/MultiTarget/MultiTarget.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DllExports" Version="*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
14
Samples/NetFramework/Class1.cs
Normal file
14
Samples/NetFramework/Class1.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using DllExports;
|
||||||
|
|
||||||
|
namespace NetFramework
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
[DllExport("MyExport", CallingConvention.StdCall)]
|
||||||
|
public static void InternalName()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Samples/NetFramework/NetFramework.csproj
Normal file
63
Samples/NetFramework/NetFramework.csproj
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<Import Project="..\..\packages\DllExports.0.1.0\build\DllExports.props" Condition="Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{E3DEABA8-40FD-45CE-8868-A7589F610007}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>NetFramework</RootNamespace>
|
||||||
|
<AssemblyName>NetFramework</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="DllExports">
|
||||||
|
<HintPath>..\..\packages\DllExports.0.1.0\lib\netstandard2.0\DllExports.dll</HintPath>
|
||||||
|
<Private>False</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Class1.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<Import Project="..\..\packages\DllExports.0.1.0\build\DllExports.targets" Condition="Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" />
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\..\packages\DllExports.0.1.0\build\DllExports.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\DllExports.0.1.0\build\DllExports.targets'))" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
36
Samples/NetFramework/Properties/AssemblyInfo.cs
Normal file
36
Samples/NetFramework/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("NetFramework")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("NetFramework")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2023")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("e3deaba8-40fd-45ce-8868-a7589f610007")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
4
Samples/NetFramework/packages.config
Normal file
4
Samples/NetFramework/packages.config
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="DllExports" version="0.1.0" targetFramework="net472" />
|
||||||
|
</packages>
|
||||||
14
Samples/SingleTarget/Class1.cs
Normal file
14
Samples/SingleTarget/Class1.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using DllExports;
|
||||||
|
|
||||||
|
namespace SingleTarget
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
[DllExport("MyExport", CallingConvention.Cdecl)]
|
||||||
|
public static void InternalName()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Samples/SingleTarget/SingleTarget.csproj
Normal file
11
Samples/SingleTarget/SingleTarget.csproj
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DllExports" Version="*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
Version.props
Normal file
8
Version.props
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>0.1.0.0</FileVersion>
|
||||||
|
<InformationalVersion>0.1.0</InformationalVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
33
appveyor.yml
Normal file
33
appveyor.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
version: 'Build #{build}'
|
||||||
|
image: Visual Studio 2019
|
||||||
|
configuration: Release
|
||||||
|
environment:
|
||||||
|
# Don't bother setting up a package cache
|
||||||
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
|
install:
|
||||||
|
- ps: . .\build.ps1
|
||||||
|
before_build:
|
||||||
|
# Restore NuGet packages
|
||||||
|
- ps: dotnet restore
|
||||||
|
build_script:
|
||||||
|
- ps: |
|
||||||
|
build
|
||||||
|
dotnet build DllExports.Tests --no-dependencies
|
||||||
|
before_test:
|
||||||
|
# Build NuGet packages
|
||||||
|
- ps: pack
|
||||||
|
test_script:
|
||||||
|
- ps: test
|
||||||
|
- vstest.console /logger:Appveyor DllExports.Tests\bin\%CONFIGURATION%\net472\DllExports.Tests.dll
|
||||||
|
- vstest.console /logger:Appveyor DllExports.Tests\bin\%CONFIGURATION%\net5.0\DllExports.Tests.dll
|
||||||
|
#on_finish:
|
||||||
|
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||||
|
artifacts:
|
||||||
|
- path: '*.nupkg'
|
||||||
|
skip_commits:
|
||||||
|
files:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.yml'
|
||||||
|
- '**/*.nuspec'
|
||||||
|
skip_tags: true
|
||||||
261
build.ps1
Normal file
261
build.ps1
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$dllExportsMSBuild = Join-Path $PSScriptRoot DllExports.MSBuild
|
||||||
|
$dllExportsMSBuildBin = Join-Path (Join-Path $dllExportsMSBuild "bin") "Release"
|
||||||
|
|
||||||
|
function build
|
||||||
|
{
|
||||||
|
dotnet build $dllExportsMSBuild -c Release
|
||||||
|
}
|
||||||
|
|
||||||
|
function pack
|
||||||
|
{
|
||||||
|
gci $PSScriptRoot *.nupkg -Recurse | foreach { Remove-Item $_.FullName -Force }
|
||||||
|
|
||||||
|
dotnet pack -c Release $dllExportsMSBuild
|
||||||
|
|
||||||
|
$nupkg = gci $dllExportsMSBuildBin *.nupkg
|
||||||
|
|
||||||
|
$originalExtension = $nupkg.Extension
|
||||||
|
$newName = $nupkg.Name -replace $originalExtension,".zip"
|
||||||
|
$newPath = Join-Path $nupkg.DirectoryName $newName
|
||||||
|
|
||||||
|
if (Test-Path $newPath)
|
||||||
|
{
|
||||||
|
Remove-item $newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$extractFolder = $nupkg.FullName -replace $nupkg.Extension,""
|
||||||
|
|
||||||
|
if (Test-Path $extractFolder)
|
||||||
|
{
|
||||||
|
Remove-Item $extractFolder -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$newItem = Rename-Item -Path $nupkg.FullName -NewName $newName -PassThru
|
||||||
|
|
||||||
|
Expand-Archive $newItem.FullName $extractFolder
|
||||||
|
|
||||||
|
$libDir = Join-Path $extractFolder "lib"
|
||||||
|
$tasksDir = Join-Path $extractFolder "tasks"
|
||||||
|
|
||||||
|
Copy-Item $libDir $tasksDir -Recurse
|
||||||
|
|
||||||
|
$badDlls = gci $libDir -Recurse -File | where Name -ne "DllExports.dll"
|
||||||
|
$badDlls | foreach { Remove-Item $_.FullName -Force }
|
||||||
|
|
||||||
|
# Tasks are not supported on netstandard2.0
|
||||||
|
Remove-Item (Join-Path $tasksDir "netstandard2.0") -Recurse -Force
|
||||||
|
|
||||||
|
# We don't need libs for net472 and net5.0
|
||||||
|
Remove-Item (Join-path $libDir "net472") -Recurse -Force
|
||||||
|
Remove-Item (Join-path $libDir "net5.0") -Recurse -Force
|
||||||
|
|
||||||
|
$nuspecFile = Join-Path $extractFolder "DllExports.nuspec"
|
||||||
|
|
||||||
|
$lines = gc $nuspecFile
|
||||||
|
$newLines = $lines | where { !$_.Contains(".NETFramework4.7.2") -and !$_.Contains("net5.0") }
|
||||||
|
$newLines | Set-Content $nuspecFile
|
||||||
|
|
||||||
|
gci $extractFolder | Compress-Archive -DestinationPath $newItem.FullName -Force
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Remove-Item $extractFolder -Recurse -Force
|
||||||
|
Rename-Item $newItem.FullName $nupkg.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
$destination = Join-Path $PSScriptRoot $nupkg.Name
|
||||||
|
Move-item $nupkg.FullName $destination
|
||||||
|
|
||||||
|
Write-Host "NuGet package created at $destination" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function test
|
||||||
|
{
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Position=0)]
|
||||||
|
[string[]]$Samples,
|
||||||
|
|
||||||
|
[string]$Configuration = "Release"
|
||||||
|
)
|
||||||
|
|
||||||
|
$kernel32 = ([System.Management.Automation.PSTypeName]"PInvoke.Kernel32").Type
|
||||||
|
|
||||||
|
if(!$kernel32)
|
||||||
|
{
|
||||||
|
$str = @"
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", EntryPoint = "LoadLibraryW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
public static extern IntPtr LoadLibrary(string lpLibFileName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool FreeLibrary(IntPtr hLibModule);
|
||||||
|
"@
|
||||||
|
|
||||||
|
$kernel32 = Add-Type -Name Kernel32 -MemberDefinition $str -Namespace "PInvoke" -PassThru
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$nupkg = gci $PSScriptRoot *.nupkg
|
||||||
|
|
||||||
|
if(!$nupkg)
|
||||||
|
{
|
||||||
|
throw "Package must be built before running tests"
|
||||||
|
}
|
||||||
|
|
||||||
|
$repoPath = Join-Path $env:temp DllExportsTempRepo
|
||||||
|
$nugetConfigPath = Join-Path $PSScriptRoot "NuGet.config"
|
||||||
|
|
||||||
|
# Cleanup any files from previous runs
|
||||||
|
|
||||||
|
if(Test-Path $repoPath)
|
||||||
|
{
|
||||||
|
Remove-Item $repoPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Test-Path $nugetConfigPath)
|
||||||
|
{
|
||||||
|
Remove-Item $nugetConfigPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$nugetCache = Join-Path (Join-Path (Join-Path $env:USERPROFILE .nuget) "packages") "dllexports"
|
||||||
|
|
||||||
|
if(Test-Path $nugetCache)
|
||||||
|
{
|
||||||
|
Remove-Item $nugetCache -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
# Add a config file pointing to a local repo containing our nupkg
|
||||||
|
|
||||||
|
nuget add $nupkg.FullName -Source $repoPath
|
||||||
|
|
||||||
|
$nugetConfig = @"
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<add key="local" value="$repoPath" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content $nugetConfigPath $nugetConfig
|
||||||
|
|
||||||
|
$sampleDirs = gci $PSScriptRoot\Samples
|
||||||
|
|
||||||
|
foreach($sample in $sampleDirs)
|
||||||
|
{
|
||||||
|
if($Samples -ne $null -and $sample.Name -notin $Samples)
|
||||||
|
{
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sample.Name -eq "NetFramework")
|
||||||
|
{
|
||||||
|
$csproj = Join-Path $sample.FullName "NetFramework.csproj"
|
||||||
|
$csprojContent = gc $csproj -Raw
|
||||||
|
$dllExportVersion = [regex]::Match($csprojContent, ".+?(DllExports\..+?)\\").groups[1].Value
|
||||||
|
|
||||||
|
$newContent = $csprojContent -replace $dllExportVersion,$nupkg.BaseName
|
||||||
|
Set-Content $csproj $newContent -NoNewline -Encoding UTF8
|
||||||
|
|
||||||
|
$packagesConfig = Join-Path $sample.FullName "packages.config"
|
||||||
|
$packagesConfigContent = gc $packagesConfig -Raw
|
||||||
|
$oldVersion = $dllExportVersion -replace "DllExports.",""
|
||||||
|
$newVersion = $nupkg.BaseName -replace "DllExports.",""
|
||||||
|
$newContent = $packagesConfigContent -replace $oldVersion,$newVersion
|
||||||
|
Set-Content $packagesConfig $newContent -NoNewline -Encoding UTF8
|
||||||
|
|
||||||
|
nuget restore $csproj -SolutionDirectory $PSScriptRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Testing sample $($sample.FullName)" -ForegroundColor Magenta
|
||||||
|
|
||||||
|
$obj = Join-Path $sample.FullName obj
|
||||||
|
$bin = Join-Path $sample.FullName bin
|
||||||
|
|
||||||
|
if (Test-Path $obj)
|
||||||
|
{
|
||||||
|
Remove-Item $obj -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $bin)
|
||||||
|
{
|
||||||
|
Remove-Item $bin -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
dotnet build $sample.FullName -c $Configuration /p:DllExportsArchitectureNameFormat="{name}" /p:DllExportsArchitectures=AMD64
|
||||||
|
|
||||||
|
if($? -eq $false)
|
||||||
|
{
|
||||||
|
throw "$($sample.Name) build failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$binDir = Join-Path $bin $Configuration
|
||||||
|
|
||||||
|
$targetFrameworks = gci $binDir -Directory
|
||||||
|
|
||||||
|
if($targetFrameworks.Length -eq 0)
|
||||||
|
{
|
||||||
|
# .NET Framework
|
||||||
|
$targetFrameworks = @(gi $binDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($targetFramework in $targetFrameworks)
|
||||||
|
{
|
||||||
|
Write-Host " Checking $targetFramework" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$dll = gci $targetFramework.FullName *.dll
|
||||||
|
|
||||||
|
if(@($dll).Count -ne 1)
|
||||||
|
{
|
||||||
|
throw "Expected exactly 1 DLL but this was not the case"
|
||||||
|
}
|
||||||
|
|
||||||
|
$hModule = $kernel32::LoadLibrary($dll.FullName)
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if($hModule)
|
||||||
|
{
|
||||||
|
$export = $kernel32::GetProcAddress($hModule, "MyExport")
|
||||||
|
|
||||||
|
if($export -ne 0)
|
||||||
|
{
|
||||||
|
Write-Host " Found 'MyExport' at 0x$($export.ToString('X'))" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw "Failed to find 'MyExport' in sample '$($sample.Name)/$($targetFramework.Name)'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw "Failed to load $($dll.FullName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
$kernel32::FreeLibrary($hModule) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if(Test-Path $repoPath)
|
||||||
|
{
|
||||||
|
Remove-Item $repoPath -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Test-Path $nugetConfigPath)
|
||||||
|
{
|
||||||
|
Remove-Item $nugetConfigPath -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user