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