Skip to content

Commit

Permalink
Make auto-compiling of C# work with .NET Core
Browse files Browse the repository at this point in the history
CodeDom doesn't work in .NET Core (dotnet/runtime#18768) and Roslyn turns out to be too low level (just a compiler and not a building framework) so the best approach is to invoke msbuild as a process.

Signed-off-by: Dimitar Dobrev <dpldobrev@protonmail.com>
  • Loading branch information
ddobrev committed Jan 2, 2021
1 parent 8451cef commit 0cb2370
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 110 deletions.
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<Project>
<ItemGroup>
<PackageVersion Include="System.CodeDom" Version="4.7.0" />
<PackageVersion Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageVersion Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="2.3.2262-g94fae01e" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
Expand Down
4 changes: 3 additions & 1 deletion build/Tests.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ function SetupTestProjectsCSharp(name, depends, extraFiles, suffix)
str = "Std"
end

SetupExternalManagedTestProject(name .. ".CSharp")
if name ~= "NamespacesDerived" then
SetupExternalManagedTestProject(name .. ".CSharp")
end
SetupExternalManagedTestProject(name .. ".Tests.CSharp")
end

Expand Down
4 changes: 0 additions & 4 deletions src/Generator/CppSharp.Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,4 @@
<ProjectReference Include="..\Parser\CppSharp.Parser.csproj" />
<ProjectReference Include="$(NativeProjectsDir)Std-symbols.vcxproj" ReferenceOutputAssembly="false" Condition="$(IsWindows)" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.CodeDom" PrivateAssets="All" />
</ItemGroup>
</Project>
90 changes: 22 additions & 68 deletions src/Generator/Driver.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using CppSharp.AST;
using CppSharp.Generators;
using CppSharp.Generators.C;
using CppSharp.Generators.CLI;
using CppSharp.Generators.Cpp;
using CppSharp.Generators.CSharp;
using CppSharp.Parser;
using CppSharp.Passes;
using CppSharp.Utils;
using Microsoft.CSharp;
using CppSharp.Types;
using CppSharp.Generators.Cpp;
using CppSharp.Generators.C;

namespace CppSharp
{
public class Driver : IDisposable
{
public DriverOptions Options { get; private set; }
public DriverOptions Options { get; }
public ParserOptions ParserOptions { get; set; }
public BindingContext Context { get; private set; }
public Generator Generator { get; private set; }
Expand Down Expand Up @@ -351,68 +348,29 @@ private void WriteGeneratedCodeToFile(string file, string generatedCode)
File.WriteAllText(file, generatedCode);
}

private static readonly Dictionary<Module, string> libraryMappings = new Dictionary<Module, string>();

public void CompileCode(Module module)
public bool CompileCode(Module module)
{
var assemblyFile = Path.Combine(Options.OutputDir, module.LibraryName + ".dll");

var docFile = Path.ChangeExtension(assemblyFile, ".xml");
File.WriteAllText(Path.Combine(Options.OutputDir, "Directory.Build.props"), "<Project />");

var compilerOptions = new StringBuilder();
compilerOptions.Append($" /doc:\"{docFile}\"");
compilerOptions.Append(" /debug:pdbonly");
compilerOptions.Append(" /unsafe");
var msBuildGenerator = new MSBuildGenerator(Context, module, libraryMappings);
msBuildGenerator.Process();
string csproj = Path.Combine(Options.OutputDir,
$"{module.LibraryName}.{msBuildGenerator.FileExtension}");
File.WriteAllText(csproj, msBuildGenerator.Generate());

var compilerParameters = new CompilerParameters
string output = ProcessHelper.Run("dotnet", $"msbuild -restore {csproj}",
out int error, out string errorMessage);
if (error == 0)
{
GenerateExecutable = false,
TreatWarningsAsErrors = false,
OutputAssembly = assemblyFile,
GenerateInMemory = false,
CompilerOptions = compilerOptions.ToString()
};

if (module != Options.SystemModule)
compilerParameters.ReferencedAssemblies.Add(
Path.Combine(Options.OutputDir, $"{Options.SystemModule.LibraryName}.dll"));
// add a reference to System.Core
compilerParameters.ReferencedAssemblies.Add(typeof(Enumerable).Assembly.Location);

var location = System.Reflection.Assembly.GetExecutingAssembly().Location;
var outputDir = Path.GetDirectoryName(location);
var locationRuntime = Path.Combine(outputDir, "CppSharp.Runtime.dll");
compilerParameters.ReferencedAssemblies.Add(locationRuntime);

compilerParameters.ReferencedAssemblies.AddRange(
(from dependency in module.Dependencies
where libraryMappings.ContainsKey(dependency)
select libraryMappings[dependency]).ToArray());

compilerParameters.ReferencedAssemblies.AddRange(module.ReferencedAssemblies.ToArray());

Diagnostics.Message($"Compiling {module.LibraryName}...");
CompilerResults compilerResults;
using (var codeProvider = new CSharpCodeProvider(
new Dictionary<string, string> {
{ "CompilerDirectoryPath", ManagedToolchain.FindCSharpCompilerDir() } }))
{
compilerResults = codeProvider.CompileAssemblyFromFile(
compilerParameters, module.CodeFiles.ToArray());
Diagnostics.Message($@"Compilation succeeded: {
libraryMappings[module] = Path.Combine(
Options.OutputDir, $"{module.LibraryName}.dll")}.");
return true;
}

var errors = compilerResults.Errors.Cast<CompilerError>().Where(e => !e.IsWarning &&
// HACK: auto-compiling on OS X produces "errors" which do not affect compilation so we ignore them
(!Platform.IsMacOS || !e.ErrorText.EndsWith("(Location of the symbol related to previous warning)", StringComparison.Ordinal))).ToList();
foreach (var error in errors)
Diagnostics.Error(error.ToString());

HasCompilationErrors = errors.Count > 0;
if (!HasCompilationErrors)
{
libraryMappings[module] = Path.Combine(outputDir, assemblyFile);
Diagnostics.Message("Compilation succeeded.");
}
Diagnostics.Error(output);
Diagnostics.Error(errorMessage);
return false;
}

public void AddTranslationUnitPass(TranslationUnitPass pass)
Expand All @@ -433,6 +391,7 @@ public void Dispose()
}

private bool hasParsingErrors;
private static readonly Dictionary<Module, string> libraryMappings = new Dictionary<Module, string>();
}

public static class ConsoleDriver
Expand Down Expand Up @@ -498,12 +457,7 @@ public static void Run(ILibrary library)

driver.SaveCode(outputs);
if (driver.Options.IsCSharpGenerator && driver.Options.CompileCode)
foreach (var module in driver.Options.Modules)
{
driver.CompileCode(module);
if (driver.HasCompilationErrors)
break;
}
driver.Options.Modules.Any(m => !driver.CompileCode(m));
}
}
}
Expand Down
55 changes: 55 additions & 0 deletions src/Generator/Generators/MSBuildGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CppSharp.AST;

namespace CppSharp.Generators
{
public class MSBuildGenerator : CodeGenerator
{
public MSBuildGenerator(BindingContext context, Module module, Dictionary<Module, string> libraryMappings)
: base(context)
{
this.module = module;
this.libraryMappings = libraryMappings;
}

public override string FileExtension => "csproj";

public override void Process()
{
var location = System.Reflection.Assembly.GetExecutingAssembly().Location;
Write($@"
<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PlatformTarget>{(Context.TargetInfo.PointerWidth == 64 ? "x64" : "x86")}</PlatformTarget>
<OutputPath>{Options.OutputDir}</OutputPath>
<DocumentationFile>{module.LibraryName}.xml</DocumentationFile>
<Configuration>Release</Configuration>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableDefaultNoneItems>false</EnableDefaultNoneItems>
<EnableDefaultItems>false</EnableDefaultItems>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
{string.Join(Environment.NewLine, module.CodeFiles.Select(c =>
$"<Compile Include=\"{c}\" />"))}
</ItemGroup>
<ItemGroup>
{string.Join(Environment.NewLine,
new[] { Path.Combine(Path.GetDirectoryName(location), "CppSharp.Runtime.dll") }
.Union(module.Dependencies.Where(libraryMappings.ContainsKey).Select(d => libraryMappings[d]))
.Select(reference =>
$@"<Reference Include=""{Path.GetFileNameWithoutExtension(reference)}"">
<HintPath>{reference}</HintPath>
</Reference>"))}
</ItemGroup>
</Project>".Trim());
}

private readonly Module module;
private readonly Dictionary<Module, string> libraryMappings;
}
}
2 changes: 1 addition & 1 deletion src/Generator/Types/Std/Stdlib.CSharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public override Type CSharpSignatureType(TypePrinterContext ctx)
if (enconding == Encoding.ASCII)
return new CustomType("[MarshalAs(UnmanagedType.LPStr)] string");
else if (enconding == Encoding.UTF8)
return new CustomType("[MarshalAs(UnmanagedType.LPUTF8Str)] string");
return new CustomType("[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CppSharp.Runtime.UTF8Marshaller))] string");
else if (enconding == Encoding.Unicode || enconding == Encoding.BigEndianUnicode)
return new CustomType("[MarshalAs(UnmanagedType.LPWStr)] string");
else if (enconding == Encoding.UTF32)
Expand Down
58 changes: 58 additions & 0 deletions src/Runtime/UTF8Marshaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace CppSharp.Runtime
{
// HACK: .NET Standard 2.0 which we use in auto-building to support .NET Framework, lacks UnmanagedType.LPUTF8Str
public class UTF8Marshaller : ICustomMarshaler
{
public void CleanUpManagedData(object ManagedObj)
{
}

public void CleanUpNativeData(IntPtr pNativeData)
=> Marshal.FreeHGlobal(pNativeData);

public int GetNativeDataSize() => -1;

public IntPtr MarshalManagedToNative(object managedObj)
{
if (managedObj == null)
return IntPtr.Zero;
if (!(managedObj is string))
throw new MarshalDirectiveException(
"UTF8Marshaler must be used on a string.");

// not null terminated
byte[] strbuf = Encoding.UTF8.GetBytes((string) managedObj);
IntPtr buffer = Marshal.AllocHGlobal(strbuf.Length + 1);
Marshal.Copy(strbuf, 0, buffer, strbuf.Length);

// write the terminating null
Marshal.WriteByte(buffer + strbuf.Length, 0);
return buffer;
}

public unsafe object MarshalNativeToManaged(IntPtr str)
{
if (str == IntPtr.Zero)
return null;

int byteCount = 0;
var str8 = (byte*) str;
while (*(str8++) != 0) byteCount += sizeof(byte);

return Encoding.UTF8.GetString((byte*) str, byteCount);
}

public static ICustomMarshaler GetInstance(string pstrCookie)
{
if (marshaler == null)
marshaler = new UTF8Marshaller();
return marshaler;
}

private static UTF8Marshaller marshaler;
}
}
12 changes: 0 additions & 12 deletions tests/NamespacesBase/NamespacesBase.CSharp.csproj

This file was deleted.

3 changes: 1 addition & 2 deletions tests/NamespacesBase/premake4.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ end

group "Tests/Namespaces"
SetupTestNativeProject("NamespacesBase")
targetdir (path.join(gendir, "NamespacesDerived"))
SetupWrapper("NamespacesBase")
targetdir (path.join(gendir, "NamespacesDerived"))
14 changes: 0 additions & 14 deletions tests/NamespacesDerived/NamespacesDerived.CSharp.csproj

This file was deleted.

2 changes: 1 addition & 1 deletion tests/NamespacesDerived/NamespacesDerived.Gen.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.IO;
using System.Reflection;
using CppSharp.AST;
using CppSharp.Generators;
using CppSharp.Utils;
Expand All @@ -18,6 +17,7 @@ public override void Setup(Driver driver)
base.Setup(driver);
driver.Options.GenerateDefaultValuesForArguments = true;
driver.Options.GenerateClassTemplates = true;
driver.Options.CompileCode = true;
driver.Options.DependentNameSpaces.Add("System.Runtime.CompilerServices");
driver.Options.Modules[1].IncludeDirs.Add(GetTestsDirectory("NamespacesDerived"));
var @base = "NamespacesBase";
Expand Down
14 changes: 13 additions & 1 deletion tests/NamespacesDerived/NamespacesDerived.Tests.CSharp.csproj
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk" />
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="NamespacesDerived.Gen.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Reference Include="NamespacesBase">
<HintPath>..\..\build\gen\NamespacesDerived\NamespacesBase.dll</HintPath>
</Reference>
<Reference Include="NamespacesDerived">
<HintPath>..\..\build\gen\NamespacesDerived\NamespacesDerived.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
5 changes: 1 addition & 4 deletions tests/NamespacesDerived/premake4.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,4 @@ group "Tests/Namespaces"
end

SetupTestGeneratorProject("NamespacesDerived")
SetupTestProjectsCSharp("NamespacesDerived", "NamespacesBase")

project("NamespacesDerived.Tests.CSharp")
links { "NamespacesBase.CSharp" }
SetupTestProjectsCSharp("NamespacesDerived", "NamespacesBase")
2 changes: 1 addition & 1 deletion tests/Test.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(TestName).CSharp.csproj" Condition="$(MSBuildProjectName.EndsWith('CSharp'))" />
<ProjectReference Include="$(TestName).CSharp.csproj" Condition="$(MSBuildProjectName.EndsWith('CSharp')) AND $(TestName) != 'NamespacesDerived'" />
<ProjectReference Include="$(NativeProjectsDir)$(TestName).Native.vcxproj" Condition="$(IsWindows)" ReferenceOutputAssembly="false" />
<ProjectReference Include="$(NativeProjectsDir)$(TestName).CLI.vcxproj" Condition="$(MSBuildProjectName.EndsWith('CLI')) AND $(IsWindows)" />
</ItemGroup>
Expand Down

0 comments on commit 0cb2370

Please sign in to comment.