Initial commit

This commit is contained in:
lordmilko
2023-07-22 22:26:44 +10:00
commit 01f2f14907
31 changed files with 1772 additions and 0 deletions

49
.gitignore vendored Normal file
View 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

View 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>

View 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;
}
}
}

View 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();
}
}

View 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

View 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

View 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

View 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>

View 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>

View 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;
}
}
}
}

View 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>

View 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);
}
}
}
}

View 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
View 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

View 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;
}
}
}

View 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>

View 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
View 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
View 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
View File

@@ -0,0 +1,111 @@
# DllExports
[![Appveyor status](https://ci.appveyor.com/api/projects/status/mgsa6414kmv4aoko?svg=true)](https://ci.appveyor.com/project/lordmilko/dllexports)
[![NuGet](https://img.shields.io/nuget/v/DllExports.svg)](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)]`

View File

@@ -0,0 +1,14 @@
using System;
using System.Runtime.InteropServices;
using DllExports;
namespace MultiTarget
{
public class Class1
{
[DllExport("MyExport")]
public static void InternalName()
{
}
}
}

View 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>

View 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()
{
}
}
}

View 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>

View 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")]

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="DllExports" version="0.1.0" targetFramework="net472" />
</packages>

View 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()
{
}
}
}

View 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
View 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
View 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
View 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
}
}
}