Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect SupportedOSPlatformAttribute on exports. #83

Merged
merged 6 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ The native API is defined in [`src/platform/dnne.h`](./src/platform/dnne.h).

The `DNNE_ASSEMBLY_NAME` must be set during compilation to indicate the name of the managed assembly to load. The assembly name should not include the extension. For example, if the managed assembly on disk is called `ClassLib.dll`, the expected assembly name is `ClassLib`.

The following defines are set based on the target OS platform:
- `DNNE_WINDOWS`
- `DNNE_OSX`
- `DNNE_LINUX`
- `DNNE_FREEBSD`

The generated source will need to be linked against the [`nethost`](https://docs.microsoft.com/dotnet/core/tutorials/netcore-hosting#create-a-host-using-nethosth-and-hostfxrh) library as either a static lib (`libnethost.[lib|a]`) or dynamic/shared library (`nethost.lib`). If the latter linking is performed, the `nethost.[dll|so|dylib]` will need to be deployed with the export binary or be on the path at run time.

The `set_failure_callback()` function can be used prior to calling an export to set a callback in the event runtime load or export discovery fails.
Expand Down Expand Up @@ -236,6 +242,8 @@ public class Exports

* I am not using one of the supported compilers and hitting an issue of missing `intptr_t` type, what can I do?
* The [C99 specification](https://en.cppreference.com/w/c/types/integer) indicates several types like `intptr_t` and `uintptr_t` are **optional**. It is recommended to override the computed type using `DNNE.C99TypeAttribute`. For example, `[DNNE.C99Type("void*")]` can be used to override an instance where `intptr_t` is generated by DNNE.
* How can I use the same export name across platforms but with different implementations?
* The .NET platform provides [`SupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.supportedosplatformattribute) and [`UnsupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.unsupportedosplatformattribute) which are fully supported by DNNE. All .NET supplied platform names are recognized. It is also possible to define your own using `C99DeclCodeAttribute`. See [`MiscExport.cs`](./test/ExportingAssembly/MiscExports.cs) for an example.

# Additional References

Expand Down
244 changes: 237 additions & 7 deletions src/dnne-gen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;

Expand All @@ -54,12 +54,22 @@ class Generator : IDisposable
private readonly string assemblyPath;
private readonly PEReader peReader;
private readonly MetadataReader mdReader;
private readonly Scope assemblyScope;
private readonly Scope moduleScope;
private readonly IDictionary<TypeDefinitionHandle, Scope> typePlatformScenarios = new Dictionary<TypeDefinitionHandle, Scope>();

public Generator(string validAssemblyPath)
{
this.assemblyPath = validAssemblyPath;
this.peReader = new PEReader(File.OpenRead(this.assemblyPath));
this.mdReader = this.peReader.GetMetadataReader(MetadataReaderOptions.None);

// Check for platform scenario attributes
AssemblyDefinition asmDef = this.mdReader.GetAssemblyDefinition();
this.assemblyScope = GetOSPlatformScope(asmDef.GetCustomAttributes());

ModuleDefinition modDef = this.mdReader.GetModuleDefinition();
this.moduleScope = GetOSPlatformScope(modDef.GetCustomAttributes());
}

public void Emit(string outputFile)
Expand Down Expand Up @@ -94,6 +104,8 @@ public void Emit(TextWriter outputStream)
continue;
}

var supported = new List<OSPlatform>();
var unsupported = new List<OSPlatform>();
var callConv = SignatureCallingConvention.Unmanaged;
var exportAttrType = ExportType.None;
string managedMethodName = this.mdReader.GetString(methodDef.Name);
Expand All @@ -105,11 +117,22 @@ public void Emit(TextWriter outputStream)
var currAttrType = this.GetExportAttributeType(customAttr);
if (currAttrType == ExportType.None)
{
// Check if method has "additional code" attributes.
// Check if method has other supported attributes.
if (TryGetC99DeclCodeAttributeValue(customAttr, out string c99Decl))
{
additionalCodeStatements.Add(c99Decl);
}
else if (TryGetOSPlatformAttributeValue(customAttr, out bool isSupported, out OSPlatform scen))
{
if (isSupported)
{
supported.Add(scen);
}
else
{
unsupported.Add(scen);
}
}

continue;
}
Expand Down Expand Up @@ -260,6 +283,17 @@ public void Emit(TextWriter outputStream)
MethodName = managedMethodName,
ExportName = exportName,
CallingConvention = callConv,
Platforms = new PlatformSupport()
{
Assembly = this.assemblyScope,
Module = this.moduleScope,
Type = GetTypeOSPlatformScope(methodDef),
Method = new Scope()
{
Support = supported,
NoSupport = unsupported,
}
},
ReturnType = returnType,
ArgumentTypes = ImmutableArray.Create(argumentTypes),
ArgumentNames = ImmutableArray.Create(argumentNames),
Expand Down Expand Up @@ -326,6 +360,98 @@ private bool TryGetC99DeclCodeAttributeValue(CustomAttribute attribute, out stri
return !string.IsNullOrEmpty(c99Decl);
}

private Scope GetTypeOSPlatformScope(MethodDefinition methodDef)
{
TypeDefinitionHandle typeDefHandle = methodDef.GetDeclaringType();
if (this.typePlatformScenarios.TryGetValue(typeDefHandle, out Scope scope))
{
return scope;
}

TypeDefinition typeDef = this.mdReader.GetTypeDefinition(typeDefHandle);
var typeScope = GetOSPlatformScope(typeDef.GetCustomAttributes());

// Record and return the scenarios.
this.typePlatformScenarios.Add(typeDefHandle, typeScope);
return typeScope;
}

private Scope GetOSPlatformScope(CustomAttributeHandleCollection attrs)
{
var supported = new List<OSPlatform>();
var unsupported = new List<OSPlatform>();
foreach (var customAttrHandle in attrs)
{
CustomAttribute customAttr = this.mdReader.GetCustomAttribute(customAttrHandle);
if (TryGetOSPlatformAttributeValue(customAttr, out bool isSupported, out OSPlatform scen))
{
if (isSupported)
{
supported.Add(scen);
}
else
{
unsupported.Add(scen);
}
}
}

return new Scope()
{
Support = supported,
NoSupport = unsupported
};
}

private bool TryGetOSPlatformAttributeValue(
CustomAttribute attribute,
out bool support,
out OSPlatform platform)
{
platform = default;

support = IsAttributeType(this.mdReader, attribute, "System.Runtime.Versioning", nameof(SupportedOSPlatformAttribute));
if (!support)
{
// If the unsupported attribute exists the "support" value is properly set.
bool nosupport = IsAttributeType(this.mdReader, attribute, "System.Runtime.Versioning", nameof(UnsupportedOSPlatformAttribute));
if (!nosupport)
{
return false;
}
}

string value = GetFirstFixedArgAsStringValue(this.typeResolver, attribute);
if (string.IsNullOrWhiteSpace(value))
{
return false;
}

const string platformPrefix = "DNNE_";
if (value.Contains(nameof(OSPlatform.Windows), StringComparison.OrdinalIgnoreCase))
{
platform = OSPlatform.Create($"{platformPrefix}{OSPlatform.Windows}");
}
else if (value.Contains(nameof(OSPlatform.OSX), StringComparison.OrdinalIgnoreCase))
{
platform = OSPlatform.Create($"{platformPrefix}{OSPlatform.OSX}");
}
else if (value.Contains(nameof(OSPlatform.Linux), StringComparison.OrdinalIgnoreCase))
{
platform = OSPlatform.Create($"{platformPrefix}{OSPlatform.Linux}");
}
else if (value.Contains(nameof(OSPlatform.FreeBSD), StringComparison.OrdinalIgnoreCase))
{
platform = OSPlatform.Create($"{platformPrefix}{OSPlatform.FreeBSD}");
}
else
{
platform = OSPlatform.Create(value);
}

return true;
}

private static string GetFirstFixedArgAsStringValue(ICustomAttributeTypeProvider<KnownType> typeResolver, CustomAttribute attribute)
{
CustomAttributeValue<KnownType> data = attribute.DecodeValue(typeResolver);
Expand Down Expand Up @@ -362,13 +488,19 @@ private static bool IsAttributeType(MetadataReader reader, CustomAttribute attri
return false;
}

#if DEBUG
string attrNamespace = reader.GetString(namespaceMaybe);
string attrName = reader.GetString(nameMaybe);
#endif
return reader.StringComparer.Equals(namespaceMaybe, targetNamespace) && reader.StringComparer.Equals(nameMaybe, targetName);
}

private const string SafeMacroRegEx = "[^a-zA-Z0-9_]";

private static void EmitC99(TextWriter outputStream, string assemblyName, IEnumerable<ExportedMethod> exports, IEnumerable<string> additionalCodeStatements)
{
// Convert the assembly name into a supported string for C99 macros.
var assemblyNameMacroSafe = Regex.Replace(assemblyName, "[^a-zA-Z0-9_]", "_");
var assemblyNameMacroSafe = Regex.Replace(assemblyName, SafeMacroRegEx, "_");
var generatedHeaderDefine = $"__DNNE_GENERATED_HEADER_{assemblyNameMacroSafe.ToUpperInvariant()}__";
var compileAsSourceDefine = "DNNE_COMPILE_AS_SOURCE";

Expand Down Expand Up @@ -468,6 +600,8 @@ private static void EmitC99(TextWriter outputStream, string assemblyName, IEnume
");
foreach (var export in exports)
{
(var preguard, var postguard) = GetC99PlatformGuards(export.Platforms);

// Create declaration and call signature.
string delim = "";
var declsig = new StringBuilder();
Expand Down Expand Up @@ -518,13 +652,13 @@ private static void EmitC99(TextWriter outputStream, string assemblyName, IEnume

// Declare export
outputStream.WriteLine(
$@"// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}
$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}
DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig});
");
{postguard}");

// Define export in implementation stream
implStream.WriteLine(
$@"// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}
$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}
static {export.ReturnType} ({callConv}* {export.ExportName}_ptr)({declsig});
DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig})
{{
Expand All @@ -534,7 +668,7 @@ private static void EmitC99(TextWriter outputStream, string assemblyName, IEnume
}}
{returnStatementKeyword}{export.ExportName}_ptr({callsig});
}}
");
{postguard}");
}

// Emit implementation closing
Expand All @@ -547,6 +681,87 @@ private static void EmitC99(TextWriter outputStream, string assemblyName, IEnume
{implStream}");
}

private static (string preguard, string postguard) GetC99PlatformGuards(in PlatformSupport platformSupport)
{
var pre = new StringBuilder();
var post = new StringBuilder();

var postAssembly = ConvertScope(platformSupport.Assembly, ref pre);
var postModule = ConvertScope(platformSupport.Module, ref pre);
var postType = ConvertScope(platformSupport.Type, ref pre);
var postMethod = ConvertScope(platformSupport.Method, ref pre);

// Append the post guards in reverse order
post.Append(postMethod);
post.Append(postType);
post.Append(postModule);
post.Append(postAssembly);

return (pre.ToString(), post.ToString());

static string ConvertScope(in Scope scope, ref StringBuilder pre)
{
(string pre_support, string post_support) = ConvertCollection(scope.Support, "(", ")");
(string pre_nosupport, string post_nosupport) = ConvertCollection(scope.NoSupport, "!(", ")");

var post = new StringBuilder();
if (!string.IsNullOrEmpty(pre_support)
|| !string.IsNullOrEmpty(pre_nosupport))
{
// Add the preamble for the guard
pre.Append("#if ");
post.Append("#endif // ");

// Append the "support" clauses because if they don't exist they are string.Empty
pre.Append(pre_support);
post.Append(post_support);

// Check if we need to chain the clauses
if (!string.IsNullOrEmpty(pre_support) && !string.IsNullOrEmpty(pre_nosupport))
{
pre.Append(" && ");
post.Append(" && ");
}

// Append the "mpsupport" clauses because if they don't exist they are string.Empty
pre.Append($"{pre_nosupport}");
post.Append($"{post_nosupport}");

pre.Append('\n');
post.Append('\n');
}

return post.ToString();
}

static (string pre, string post) ConvertCollection(in IEnumerable<OSPlatform> platforms, in string prefix, in string suffix)
{
var pre = new StringBuilder();
var post = new StringBuilder();

var delim = prefix;
foreach (OSPlatform os in platforms)
{
if (pre.Length != 0)
{
delim = " || ";
}

var platformMacroSafe = Regex.Replace(os.ToString(), SafeMacroRegEx, "_").ToUpperInvariant();
pre.Append($"{delim}defined({platformMacroSafe})");
post.Append($"{post}{delim}{platformMacroSafe}");
}

if (pre.Length != 0)
{
pre.Append(suffix);
post.Append(suffix);
}

return (pre.ToString(), post.ToString());
}
}

private static string GetC99CallConv(SignatureCallingConvention callConv)
{
return callConv switch
Expand All @@ -560,13 +775,28 @@ private static string GetC99CallConv(SignatureCallingConvention callConv)
};
}

private struct PlatformSupport
{
public Scope Assembly { get; init; }
public Scope Module { get; init; }
public Scope Type { get; init; }
public Scope Method { get; init; }
}

private struct Scope
{
public IEnumerable<OSPlatform> Support { get; init; }
public IEnumerable<OSPlatform> NoSupport { get; init; }
}

private class ExportedMethod
{
public ExportType Type { get; init; }
public string EnclosingTypeName { get; init; }
public string MethodName { get; init; }
public string ExportName { get; init; }
public SignatureCallingConvention CallingConvention { get; init; }
public PlatformSupport Platforms { get; init; }
public string ReturnType { get; init; }
public ImmutableArray<string> ArgumentTypes { get; init; }
public ImmutableArray<string> ArgumentNames { get; init; }
Expand Down
4 changes: 1 addition & 3 deletions src/dnne-gen/dnne-gen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>DNNE</RootNamespace>

<!-- Specify the minimum version -->
<RuntimeFrameworkVersion>5.0.0-preview.5.20278.1</RuntimeFrameworkVersion>
<RollForward>major</RollForward>
</PropertyGroup>

</Project>
2 changes: 1 addition & 1 deletion src/msbuild/DNNE.props
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ DNNE.props
<DnneAdditionalIncludeDirectories></DnneAdditionalIncludeDirectories>

<!-- Indicate the dnne-gen tool's roll forward policy. -->
<DnneGenRollForward>Minor</DnneGenRollForward>
<DnneGenRollForward></DnneGenRollForward>

<!-- EXPERIMENTAL: The native hosting should assume it is in a self-contained scenario.
When setting this flag to true, the only change in output will be the generated hosting
Expand Down