Skip to content

Commit

Permalink
Improve WinRT interop support
Browse files Browse the repository at this point in the history
In order to support the interop between Win32 and WinRT objects, I've added a custom marshaler that gets brought it when needed.

I also added a new test project to demonstrate WinRT interop.
  • Loading branch information
sotteson1 authored Sep 1, 2021
2 parents 0c334fa + 2ef717a commit 3dd02d5
Show file tree
Hide file tree
Showing 10 changed files with 562 additions and 11 deletions.
31 changes: 31 additions & 0 deletions Microsoft.Windows.CsWin32.sln
Original file line number Diff line number Diff line change
Expand Up @@ -39,32 +39,62 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerationSandbox.Tests", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpellChecker", "test\SpellChecker\SpellChecker.csproj", "{744BE74F-8C4A-49E8-9683-52D987224285}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinRTInteropTest", "test\WinRTInteropTest\WinRTInteropTest.csproj", "{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|NonWindows = Debug|NonWindows
Release|Any CPU = Release|Any CPU
Release|NonWindows = Release|NonWindows
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Debug|NonWindows.Build.0 = Debug|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Release|Any CPU.Build.0 = Release|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Release|NonWindows.ActiveCfg = Release|Any CPU
{E3E96466-44B6-41AF-BBC8-9D30183ED8A9}.Release|NonWindows.Build.0 = Release|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Debug|NonWindows.Build.0 = Debug|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|Any CPU.Build.0 = Release|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|NonWindows.ActiveCfg = Release|Any CPU
{0129FE6E-3480-408A-BF40-9E6343CDB06C}.Release|NonWindows.Build.0 = Release|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Debug|NonWindows.Build.0 = Debug|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Release|Any CPU.Build.0 = Release|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Release|NonWindows.ActiveCfg = Release|Any CPU
{6638957D-09ED-47C1-86B9-5D2DFD0FE625}.Release|NonWindows.Build.0 = Release|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Debug|NonWindows.Build.0 = Debug|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Release|Any CPU.Build.0 = Release|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Release|NonWindows.ActiveCfg = Release|Any CPU
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3}.Release|NonWindows.Build.0 = Release|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Debug|Any CPU.Build.0 = Debug|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Debug|NonWindows.Build.0 = Debug|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Release|Any CPU.ActiveCfg = Release|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Release|Any CPU.Build.0 = Release|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Release|NonWindows.ActiveCfg = Release|Any CPU
{744BE74F-8C4A-49E8-9683-52D987224285}.Release|NonWindows.Build.0 = Release|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Debug|NonWindows.ActiveCfg = Debug|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Release|Any CPU.Build.0 = Release|Any CPU
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8}.Release|NonWindows.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -75,6 +105,7 @@ Global
{6638957D-09ED-47C1-86B9-5D2DFD0FE625} = {9E154A29-1796-4B85-BD81-B6A385D8FF71}
{7E8A5179-F94C-410F-8BBE-FDAAA95A19C3} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{744BE74F-8C4A-49E8-9683-52D987224285} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
{0E067B66-C2EC-4106-87D2-5310CFCDC5B8} = {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E3944F6A-384B-4B0F-B93F-3BD513DC57BD}
Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
vmImage: Ubuntu 18.04
variables:
TestFilter: "&WindowsOnly!=true"
Platform: NonWindows
steps:
- checkout: self
clean: true
Expand All @@ -37,6 +38,7 @@ jobs:
vmImage: macOS-10.15
variables:
TestFilter: "&WindowsOnly!=true"
Platform: NonWindows
steps:
- checkout: self
clean: true
Expand Down
78 changes: 67 additions & 11 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class Generator : IDisposable
internal const string RAIIFreeAttribute = "RAIIFreeAttribute";
internal const string GlobalNamespacePrefix = "global::";
internal const string GlobalWin32NamespaceAlias = "win32";
internal const string WinRTCustomMarshalerClass = "WinRTCustomMarshaler";
internal const string WinRTCustomMarshalerNamespace = "Windows.Win32.CsWin32.InteropServices";
internal const string WinRTCustomMarshalerFullName = WinRTCustomMarshalerNamespace + "." + WinRTCustomMarshalerClass;

internal static readonly SyntaxAnnotation IsManagedTypeAnnotation = new SyntaxAnnotation("IsManagedType");
internal static readonly SyntaxAnnotation IsSafeHandleTypeAnnotation = new SyntaxAnnotation("IsSafeHandleType");
Expand Down Expand Up @@ -281,6 +284,7 @@ public class Generator : IDisposable
private readonly bool generateDefaultDllImportSearchPathsAttribute;
private readonly GeneratedCode committedCode = new();
private readonly GeneratedCode volatileCode;
private bool needsWinRTCustomMarshaler;

/// <summary>
/// Initializes a new instance of the <see cref="Generator"/> class.
Expand Down Expand Up @@ -967,6 +971,28 @@ nsContents.Key is object
}
});

if (this.needsWinRTCustomMarshaler)
{
string? marshalerText = this.FetchTemplateText(WinRTCustomMarshalerClass);
if (marshalerText == null)
{
throw new GenerationFailedException($"Failed to get template for \"{WinRTCustomMarshalerClass}\".");
}

var marshalerContents = SyntaxFactory.ParseSyntaxTree(marshalerText);
if (marshalerContents == null)
{
throw new GenerationFailedException($"Failed adding \"{WinRTCustomMarshalerClass}\".");
}

var compilationUnit = ((CompilationUnitSyntax)marshalerContents.GetRoot())
.WithLeadingTrivia(ParseLeadingTrivia(AutoGeneratedHeader));

normalizedResults.Add(
string.Format(CultureInfo.InvariantCulture, FilenamePattern, WinRTCustomMarshalerClass),
compilationUnit);
}

return normalizedResults;
}

Expand All @@ -979,7 +1005,7 @@ nsContents.Key is object
}

// TODO: fill in more properties to match the original
return MarshalAs(marshalAs.Value);
return MarshalAs(marshalAs.Value, marshalAs.MarshalCookie, marshalAs.MarshalType);
}

internal static TypeSyntax MakeSpanOfT(TypeSyntax typeArgument) => GenericName("Span").AddTypeArgumentListArguments(typeArgument);
Expand Down Expand Up @@ -1184,6 +1210,9 @@ internal void RequestInteropType(TypeDefinitionHandle typeDefHandle)
new SyntaxAnnotation(NamespaceContainerAnnotation, shortNamespace));
}

this.needsWinRTCustomMarshaler |= typeDeclaration.DescendantNodes().OfType<AttributeSyntax>()
.Any(a => a.Name.ToString() == "MarshalAs" && a.ToString().Contains(WinRTCustomMarshalerFullName));

this.volatileCode.AddInteropType(typeDefHandle, typeDeclaration);
}
});
Expand Down Expand Up @@ -1767,14 +1796,31 @@ private static AttributeSyntax UnmanagedFunctionPointer(CallingConvention callin
IdentifierName(Enum.GetName(typeof(CallingConvention), callingConvention)!))));
}

private static AttributeSyntax MarshalAs(UnmanagedType unmanagedType)
private static AttributeSyntax MarshalAs(UnmanagedType unmanagedType, string marshalCookie, string marshalType)
{
return Attribute(IdentifierName("MarshalAs"))
.AddArgumentListArguments(AttributeArgument(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(nameof(UnmanagedType)),
IdentifierName(Enum.GetName(typeof(UnmanagedType), unmanagedType)!))));
var marshalAs =
Attribute(IdentifierName("MarshalAs"))
.AddArgumentListArguments(AttributeArgument(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(nameof(UnmanagedType)),
IdentifierName(Enum.GetName(typeof(UnmanagedType), unmanagedType)!))));

if (!string.IsNullOrEmpty(marshalCookie))
{
marshalAs = marshalAs.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(marshalCookie)))
.WithNameEquals(NameEquals(nameof(MarshalAsAttribute.MarshalCookie))));
}

if (!string.IsNullOrEmpty(marshalType))
{
marshalAs = marshalAs.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(marshalType)))
.WithNameEquals(NameEquals(nameof(MarshalAsAttribute.MarshalType))));
}

return marshalAs;
}

private static AttributeSyntax DebuggerBrowsable(DebuggerBrowsableState state)
Expand Down Expand Up @@ -2150,17 +2196,27 @@ private MemberDeclarationSyntax FetchTemplate(string name)
return result;
}

private bool TryFetchTemplate(string name, [NotNullWhen(true)] out MemberDeclarationSyntax? member)
private string? FetchTemplateText(string name)
{
using Stream? templateStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.templates.{name.Replace('/', '.')}.cs");
if (templateStream is null)
{
return null;
}

using StreamReader sr = new(templateStream);
return sr.ReadToEnd().Replace("\r\n", "\n").Replace("\t", string.Empty);
}

private bool TryFetchTemplate(string name, [NotNullWhen(true)] out MemberDeclarationSyntax? member)
{
string? template = this.FetchTemplateText(name);
if (template == null)
{
member = null;
return false;
}

using StreamReader sr = new(templateStream);
string template = sr.ReadToEnd().Replace("\r\n", "\n").Replace("\t", string.Empty);
member = ParseMemberDeclaration(template) ?? throw new GenerationFailedException($"Unable to parse a type from a template: {name}");
member = this.ElevateVisibility(member);
return true;
Expand Down
25 changes: 25 additions & 0 deletions src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ internal record HandleTypeHandleInfo : TypeHandleInfo
{
private readonly MetadataReader reader;

// We just want to see that the identifier starts with I, followed by another upper case letter,
// followed by a lower case letter. All the WinRT interfaces will match this, and none of the WinRT
// objects will match it
private static readonly System.Text.RegularExpressions.Regex InterfaceNameMatcher = new System.Text.RegularExpressions.Regex("^I[A-Z][a-z]");

internal HandleTypeHandleInfo(MetadataReader reader, EntityHandle handle, byte? rawTypeKind = null)
{
this.reader = reader;
Expand Down Expand Up @@ -104,6 +109,26 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs
syntax = inputs.AllowMarshaling && !isNonCOMConformingInterface ? syntax.WithAdditionalAnnotations(Generator.IsManagedTypeAnnotation) : PointerType(syntax);
}

if (nameSyntax is QualifiedNameSyntax qualifiedName)
{
var ns = qualifiedName.Left.ToString();

// Look for WinRT namespaces
if (ns.StartsWith("Windows.Foundation") || ns.StartsWith("Windows.UI") || ns.StartsWith("Windows.Graphics") || ns.StartsWith("Windows.System"))
{
// We only want to marshal WinRT objects, not interfaces. We don't have a good way of knowing
// whether it's an interface or an object. "isInterface" comes back as false for a WinRT interface,
// so that doesn't help. Looking at the name should be good enough, but if we needed to, the
// Win32 projection could give us an attribute to make sure
var objName = qualifiedName.Right.ToString();
bool isInterfaceName = InterfaceNameMatcher.IsMatch(objName);
if (!isInterfaceName)
{
return new TypeSyntaxAndMarshaling(syntax, new MarshalAsAttribute(UnmanagedType.CustomMarshaler) { MarshalCookie = nameSyntax.ToString(), MarshalType = Generator.WinRTCustomMarshalerFullName });
}
}
}

return new TypeSyntaxAndMarshaling(syntax);
}

Expand Down
64 changes: 64 additions & 0 deletions src/Microsoft.Windows.CsWin32/templates/WinRTCustomMarshaler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Windows.Win32.CsWin32.InteropServices
{
internal class WinRTCustomMarshaler : global::System.Runtime.InteropServices.ICustomMarshaler
{
private string winrtClassName;
private bool lookedForFromAbi;
private global::System.Reflection.MethodInfo fromAbi;

private WinRTCustomMarshaler(string cookie)
{
this.winrtClassName = cookie;
}

/// <summary>
/// Gets an instance of the marshaler given a cookie
/// </summary>
/// <param name="cookie">Cookie used to create marshaler</param>
/// <returns>Marshaler</returns>
public static global::System.Runtime.InteropServices.ICustomMarshaler GetInstance(string cookie)
{
return new WinRTCustomMarshaler(cookie);
}

void global::System.Runtime.InteropServices.ICustomMarshaler.CleanUpManagedData(object ManagedObj)
{
}

void global::System.Runtime.InteropServices.ICustomMarshaler.CleanUpNativeData(global::System.IntPtr pNativeData)
{
global::System.Runtime.InteropServices.Marshal.Release(pNativeData);
}

int global::System.Runtime.InteropServices.ICustomMarshaler.GetNativeDataSize()
{
throw new global::System.NotImplementedException();
}

global::System.IntPtr global::System.Runtime.InteropServices.ICustomMarshaler.MarshalManagedToNative(object ManagedObj)
{
throw new global::System.NotImplementedException();
}

object global::System.Runtime.InteropServices.ICustomMarshaler.MarshalNativeToManaged(global::System.IntPtr pNativeData)
{
if (!this.lookedForFromAbi)
{
var assembly = typeof(global::Windows.Foundation.IMemoryBuffer).Assembly;
var type = global::System.Type.GetType($"{this.winrtClassName}, {assembly.FullName}");

this.fromAbi = type.GetMethod("FromAbi");
this.lookedForFromAbi = true;
}

if (this.fromAbi != null)
{
return this.fromAbi.Invoke(null, new object[] { pNativeData });
}
else
{
return global::System.Runtime.InteropServices.Marshal.GetObjectForIUnknown(pNativeData);
}
}
}
}
Loading

0 comments on commit 3dd02d5

Please sign in to comment.