Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,10 @@ private static MemberDeclarationSyntax PrintGeneratedSource(
Attribute(IdentifierName(Constants.DebuggerNonUserCodeAttribute))))))
.WithParameterList(ParameterList(SingletonSeparatedList(
Parameter(Identifier(Constants.ArgumentsBuffer)).WithType(PointerType(ParseTypeName(Constants.JSMarshalerArgumentGlobal))))))
.WithBody(wrapperStatements);
// The modifier above states the contract for callers; the body needs a context of its own.
.WithBody(wrapperStatements.WrapInUnsafeBlock());

MemberDeclarationSyntax toPrint = containingSyntaxContext.WrapMembersInContainingSyntaxWithUnsafeModifier(wrappperMethod);
MemberDeclarationSyntax toPrint = containingSyntaxContext.WrapMembersInContainingSyntax(wrappperMethod);

return toPrint;
}
Expand Down Expand Up @@ -262,9 +263,10 @@ private static NamespaceDeclarationSyntax GenerateRegSource(
var ns = NamespaceDeclaration(IdentifierName(generatedNamespace))
.WithMembers(
SingletonList<MemberDeclarationSyntax>(
// None of the members below name a pointer type, so the class needs no 'unsafe'
// modifier. Under the updated memory safety rules one on a type means nothing at
// all, and it never established a context for the members in the first place.
ClassDeclaration(initializerClass)
.WithModifiers(TokenList(new SyntaxToken[]{
Token(SyntaxKind.UnsafeKeyword)}))
.WithMembers(List(new[] { field, initializerMethod, method }))
.WithAttributeLists(SingletonList(AttributeList(SingletonSeparatedList(
Attribute(IdentifierName(Constants.CompilerGeneratedAttributeGlobal)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ private static MemberDeclarationSyntax PrintGeneratedSource(
}))
.WithModifiers(StripTriviaFromModifiers(userDeclaredMethod.Modifiers))
.WithParameterList(ParameterList(SeparatedList(stub.SignatureContext.StubParameters)))
.WithBody(stubCode);
// The body is wrapped in an unsafe block rather than relying on an unsafe modifier on the
// containing type, which establishes no context for it under the updated memory safety rules.
// The marshallers call helpers such as Unsafe.SkipInit that are becoming caller-unsafe, so the
// block is emitted even for the shapes whose bodies name no pointer type today.
.WithBody(stubCode.WrapInUnsafeBlock());

FieldDeclarationSyntax sigField = FieldDeclaration(VariableDeclaration(IdentifierName(Constants.JSFunctionSignatureGlobal))
.WithVariables(SingletonSeparatedList(VariableDeclarator(Identifier(stub.BindingName)))))
.AddModifiers(Token(SyntaxKind.StaticKeyword));

MemberDeclarationSyntax toPrint = containingSyntaxContext.WrapMembersInContainingSyntaxWithUnsafeModifier(stubMethod, sigField);
MemberDeclarationSyntax toPrint = containingSyntaxContext.WrapMembersInContainingSyntax(stubMethod, sigField);
return toPrint;
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Compile Include="Fails.cs" />
<Compile Include="Compiles.cs" />
<Compile Include="JSTestUtils.cs" />
<Compile Include="UnsafeCodeGeneration.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Interop.UnitTests;
using Xunit;

namespace JSImportGenerator.Unit.Tests
{
/// <summary>
/// Verifies that the generated output compiles under the updated memory safety rules ("unsafe evolution"),
/// where an <c>unsafe</c> modifier on a type establishes no context for the members inside it.
/// </summary>
public class UnsafeCodeGeneration
{
public static IEnumerable<object[]> Snippets()
{
yield return new object[] { nameof(CodeSnippets.AllDefault), CodeSnippets.AllDefault };
yield return new object[] { nameof(CodeSnippets.AllAnnotated), CodeSnippets.AllAnnotated };
yield return new object[] { nameof(CodeSnippets.AllAnnotatedExport), CodeSnippets.AllAnnotatedExport };
}

[Theory]
[MemberData(nameof(Snippets))]
public void GeneratedOutputCompilesUnderUpdatedRules(string name, string source)
{
_ = name;

Compilation comp = TestUtils.CreateCompilation(source, allowUnsafe: true);

// Roslyn does not expose the memory safety rules version through a public API yet, so opt in through
// the same feature flag the compiler uses. It lives on the parse options, so every tree is re-parsed.
var parseOptions = ((CSharpParseOptions)comp.SyntaxTrees.First().Options)
.WithFeatures([new KeyValuePair<string, string>("updated-memory-safety-rules", "")]);
comp = comp.RemoveAllSyntaxTrees().AddSyntaxTrees(
comp.SyntaxTrees.Select(t => CSharpSyntaxTree.ParseText(t.GetText(), parseOptions, t.FilePath)));

// CS9377 ("the 'unsafe' modifier does not have any effect here") sits above the default warning
// level, so it has to be raised or the assertion below could never observe it.
comp = comp.WithOptions(((CSharpCompilationOptions)comp.Options).WithWarningLevel(9999));

Compilation newComp = TestUtils.RunGenerators(comp, out var generatorDiags,
new Microsoft.Interop.JavaScript.JSImportGenerator(),
new Microsoft.Interop.JavaScript.JSExportGenerator());

Assert.Empty(generatorDiags);

// CS9377 reports an 'unsafe' modifier that has no effect under these rules. It is suppressed in
// generated files by default, so it is asserted on explicitly rather than left to the error check.
var unexpected = newComp.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error || d.Id is "CS9377")
.Select(d => $"{d.Id}: {d.GetMessage()} @ {d.Location.GetLineSpan()}")
.ToList();

Assert.Empty(unexpected);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CodeDom.Compiler;
Expand Down Expand Up @@ -51,12 +51,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
using IndentedTextWriter writer = new(sw);
writer.WriteLine("// <auto-generated />");

writer.WriteLine($"file sealed unsafe class {ClassInfoTypeName} : global::System.Runtime.InteropServices.Marshalling.IComExposedClass");
writer.WriteLine($"file sealed class {ClassInfoTypeName} : global::System.Runtime.InteropServices.Marshalling.IComExposedClass");
writer.WriteLine('{');
writer.Indent++;
writer.WriteLine("private static volatile global::System.Runtime.InteropServices.ComWrappers.ComInterfaceEntry* s_vtables;");
writer.WriteLine("private static volatile unsafe global::System.Runtime.InteropServices.ComWrappers.ComInterfaceEntry* s_vtables;");
sw.WriteLine();
writer.WriteLine("public static global::System.Runtime.InteropServices.ComWrappers.ComInterfaceEntry* GetComInterfaceEntries(out int count)");
writer.WriteLine("public static unsafe global::System.Runtime.InteropServices.ComWrappers.ComInterfaceEntry* GetComInterfaceEntries(out int count)");
writer.WriteLine('{');
writer.Indent++;
// The modifiers above make the pointer types legal to name; the body still needs an unsafe
// context of its own, since under the updated rules a member modifier opens none for it.
writer.WriteLine("unsafe");
writer.WriteLine('{');
writer.Indent++;
writer.WriteLine($"count = {implementedInterfaces.Length};");
Expand Down Expand Up @@ -84,10 +89,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
writer.WriteLine('}');
writer.Indent--;
writer.WriteLine('}');
writer.Indent--;
writer.WriteLine('}');

sw.WriteLine();

data.ContainingSyntaxContext.WriteToWithUnsafeModifier(writer, data.ClassSyntax, static (writer, classSyntax) =>
data.ContainingSyntaxContext.WriteToWithUnsafeModifier(data.UseUpdatedMemorySafetyRules, writer, data.ClassSyntax, static (writer, classSyntax) =>
{
writer.WriteLine($"[global::System.Runtime.InteropServices.Marshalling.ComExposedClassAttribute<{ClassInfoTypeName}>]");
writer.WriteLine($"{string.Join(" ", classSyntax.Modifiers)} class {classSyntax.Identifier}{classSyntax.TypeParameters} {{ }}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal sealed class ComClassInfo : IEquatable<ComClassInfo>
public ContainingSyntax ClassSyntax { get; init; }
public SequenceEqualImmutableArray<string> ImplementedInterfacesNames { get; init; }

/// <inheritdoc cref="ComInterfaceInfo.UseUpdatedMemorySafetyRules"/>
public bool UseUpdatedMemorySafetyRules { get; init; }

private ComClassInfo(string className, ContainingSyntaxContext containingSyntaxContext, ContainingSyntax classSyntax, SequenceEqualImmutableArray<string> implementedInterfacesNames)
{
ClassName = className;
Expand Down Expand Up @@ -44,14 +47,18 @@ public static ComClassInfo From(INamedTypeSymbol type, ClassDeclarationSyntax sy
type.ToDisplayString(),
new ContainingSyntaxContext(syntax),
new ContainingSyntax(syntax.Modifiers, syntax.Kind(), syntax.Identifier, syntax.TypeParameterList),
new(names.ToImmutable()));
new(names.ToImmutable()))
{
UseUpdatedMemorySafetyRules = syntax.SyntaxTree.Options.Features.ContainsKey("updated-memory-safety-rules")
};
}

public bool Equals(ComClassInfo? other)
{
return other is not null
&& ClassName == other.ClassName
&& ContainingSyntaxContext.Equals(other.ContainingSyntaxContext)
&& UseUpdatedMemorySafetyRules == other.UseUpdatedMemorySafetyRules
&& ImplementedInterfacesNames.SequenceEqual(other.ImplementedInterfacesNames);
}

Expand Down
Loading