Skip to content

Commit

Permalink
Emit entrypoint to ref assemblies (#76783)
Browse files Browse the repository at this point in the history
* Emit entrypoint to ref assemblies

* Improve code

* Fix async Main

* Document the breaking change

* Improve

* Check there are no entry point errors

* Test debug entrypoint
  • Loading branch information
jjonescz authored Feb 4, 2025
1 parent d3d183e commit c46ba8b
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 18 deletions.
23 changes: 23 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,26 @@ unsafe record struct R(
public bool Equals(R other) => true;
}
```

## Emitting metadata-only executables requires an entrypoint

***Introduced in Visual Studio 2022 version 17.14***

Previously, the entrypoint was [unintentionally unset](https://github.com/dotnet/roslyn/issues/76707)
when emitting executables in metadata-only mode (also known as ref assemblies).
That is now corrected but it also means that a missing entrypoint is a compilation error:

```cs
// previously successful, now fails:
CSharpCompilation.Create("test").Emit(new MemoryStream(),
options: EmitOptions.Default.WithEmitMetadataOnly(true))

CSharpCompilation.Create("test",
// workaround - mark as DLL instead of EXE (the default):
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.Emit(new MemoryStream(),
options: EmitOptions.Default.WithEmitMetadataOnly(true))
```

Similarly this can be observed when using the command-line argument `/refonly`
or the `ProduceOnlyReferenceAssembly` MSBuild property.
23 changes: 23 additions & 0 deletions src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3503,6 +3503,29 @@ internal override bool CompileMethods(
}

SynthesizedMetadataCompiler.ProcessSynthesizedMembers(this, moduleBeingBuilt, cancellationToken);

if (moduleBeingBuilt.OutputKind.IsApplication())
{
var entryPointDiagnostics = BindingDiagnosticBag.GetInstance(withDiagnostics: true, withDependencies: false);
var entryPoint = MethodCompiler.GetEntryPoint(
this,
moduleBeingBuilt,
hasDeclarationErrors: false,
emitMethodBodies: false,
entryPointDiagnostics,
cancellationToken);
diagnostics.AddRange(entryPointDiagnostics.DiagnosticBag!);
bool shouldSetEntryPoint = entryPoint != null && !entryPointDiagnostics.HasAnyErrors();
entryPointDiagnostics.Free();
if (shouldSetEntryPoint)
{
moduleBeingBuilt.SetPEEntryPoint(entryPoint, diagnostics);
}
else
{
return false;
}
}
}
else
{
Expand Down
3 changes: 2 additions & 1 deletion src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ public static void CompileMethodBodies(

// Returns the MethodSymbol for the assembly entrypoint. If the user has a Task returning main,
// this function returns the synthesized Main MethodSymbol.
private static MethodSymbol GetEntryPoint(CSharpCompilation compilation, PEModuleBuilder moduleBeingBuilt, bool hasDeclarationErrors, bool emitMethodBodies, BindingDiagnosticBag diagnostics, CancellationToken cancellationToken)
internal static MethodSymbol GetEntryPoint(CSharpCompilation compilation, PEModuleBuilder moduleBeingBuilt, bool hasDeclarationErrors, bool emitMethodBodies, BindingDiagnosticBag diagnostics, CancellationToken cancellationToken)
{
Debug.Assert(diagnostics.DiagnosticBag != null);

Expand Down Expand Up @@ -252,6 +252,7 @@ private static MethodSymbol GetEntryPoint(CSharpCompilation compilation, PEModul
if (((object)synthesizedEntryPoint != null) &&
(moduleBeingBuilt != null) &&
!hasDeclarationErrors &&
!moduleBeingBuilt.EmitOptions.EmitMetadataOnly &&
!diagnostics.HasAnyErrors())
{
BoundStatement body = synthesizedEntryPoint.CreateBody(diagnostics);
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Test/Emit/Emit/CompilationEmitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,8 @@ internal static void Main()
VerifyMethods(output, "C", new[] { "void C.Main()", "C..ctor()" });
VerifyMvid(output, hasMvidSection: false);

verifyEntryPoint(metadataOutput, expectZero: true);
VerifyMethods(metadataOutput, "C", new[] { "C..ctor()" });
verifyEntryPoint(metadataOutput, expectZero: false);
VerifyMethods(metadataOutput, "C", new[] { "void C.Main()", "C..ctor()" });
VerifyMvid(metadataOutput, hasMvidSection: true);
}

Expand Down
219 changes: 219 additions & 0 deletions src/Compilers/CSharp/Test/Emit/Emit/EmitMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3507,5 +3507,224 @@ public class InvalidOperationException();
// warning CS8021: No value for RuntimeMetadataVersion found. No assembly containing System.Object was found nor was a value for RuntimeMetadataVersion specified through options.
Diagnostic(ErrorCode.WRN_NoRuntimeMetadataVersion).WithLocation(1, 1));
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void EmitMetadataOnly_Exe()
{
CompileAndVerify("""
System.Console.WriteLine("a");
""",
options: TestOptions.ReleaseExe.WithMetadataImportOptions(MetadataImportOptions.All),
emitOptions: EmitOptions.Default.WithEmitMetadataOnly(true),
symbolValidator: static (ModuleSymbol module) =>
{
Assert.NotEqual(0, module.GetMetadata().Module.PEReaderOpt.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress);
var main = module.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>$");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
})
.VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void EmitMetadataOnly_Exe_AsyncMain()
{
CompileAndVerify("""
using System.Threading.Tasks;
static class Program
{
static async Task Main()
{
await Task.Yield();
System.Console.WriteLine("a");
}
}
""",
options: TestOptions.ReleaseExe.WithMetadataImportOptions(MetadataImportOptions.All),
emitOptions: EmitOptions.Default.WithEmitMetadataOnly(true),
symbolValidator: static (ModuleSymbol module) =>
{
Assert.NotEqual(0, module.GetMetadata().Module.PEReaderOpt.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress);
var main = module.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
})
.VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void EmitMetadataOnly_Exe_NoMain()
{
var emitResult = CreateCompilation("""
class Program;
""",
options: TestOptions.ReleaseExe)
.Emit(new MemoryStream(), options: EmitOptions.Default.WithEmitMetadataOnly(true));
Assert.False(emitResult.Success);
emitResult.Diagnostics.Verify(
// error CS5001: Program does not contain a static 'Main' method suitable for an entry point
Diagnostic(ErrorCode.ERR_NoEntryPoint).WithLocation(1, 1));
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void EmitMetadataOnly_Exe_PrivateMain_ExcludePrivateMembers()
{
CompileAndVerify("""
static class Program
{
private static void Main() { }
}
""",
options: TestOptions.ReleaseExe.WithMetadataImportOptions(MetadataImportOptions.All),
emitOptions: EmitOptions.Default
.WithEmitMetadataOnly(true)
.WithIncludePrivateMembers(false),
symbolValidator: static (ModuleSymbol module) =>
{
Assert.NotEqual(0, module.GetMetadata().Module.PEReaderOpt.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress);
var main = module.GlobalNamespace.GetMember<MethodSymbol>("Program.Main");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
})
.VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void EmitMetadataOnly_Exe_PrivateMain_ExcludePrivateMembers_AsyncMain()
{
CompileAndVerify("""
using System.Threading.Tasks;
static class Program
{
private static async Task Main()
{
await Task.Yield();
}
}
""",
options: TestOptions.ReleaseExe.WithMetadataImportOptions(MetadataImportOptions.All),
emitOptions: EmitOptions.Default
.WithEmitMetadataOnly(true)
.WithIncludePrivateMembers(false),
symbolValidator: static (ModuleSymbol module) =>
{
Assert.NotEqual(0, module.GetMetadata().Module.PEReaderOpt.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress);
var main = module.GlobalNamespace.GetMember<MethodSymbol>("Program.<Main>");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
})
.VerifyDiagnostics();
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void ExcludePrivateMembers_PrivateMain()
{
using var peStream = new MemoryStream();
using var metadataStream = new MemoryStream();
var comp = CreateCompilation("""
static class Program
{
private static void Main() { }
}
""",
options: TestOptions.ReleaseExe);
var emitResult = comp.Emit(
peStream: peStream,
metadataPEStream: metadataStream,
options: EmitOptions.Default.WithIncludePrivateMembers(false));
Assert.True(emitResult.Success);
emitResult.Diagnostics.Verify();

verify(peStream);
verify(metadataStream);

CompileAndVerify(comp).VerifyDiagnostics();

static void verify(Stream stream)
{
stream.Position = 0;
Assert.NotEqual(0, new PEHeaders(stream).CorHeader.EntryPointTokenOrRelativeVirtualAddress);

stream.Position = 0;
var reference = AssemblyMetadata.CreateFromStream(stream).GetReference();
var comp = CreateCompilation("", references: [reference],
options: TestOptions.DebugDll.WithMetadataImportOptions(MetadataImportOptions.All));
var main = comp.GetMember<MethodSymbol>("Program.Main");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
}
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void ExcludePrivateMembers_PrivateMain_AsyncMain()
{
using var peStream = new MemoryStream();
using var metadataStream = new MemoryStream();
var comp = CreateCompilation("""
using System.Threading.Tasks;
static class Program
{
private static async Task Main()
{
await Task.Yield();
}
}
""",
options: TestOptions.ReleaseExe);
var emitResult = comp.Emit(
peStream: peStream,
metadataPEStream: metadataStream,
options: EmitOptions.Default.WithIncludePrivateMembers(false));
Assert.True(emitResult.Success);
emitResult.Diagnostics.Verify();

verify(peStream);
verify(metadataStream);

CompileAndVerify(comp).VerifyDiagnostics();

static void verify(Stream stream)
{
stream.Position = 0;
Assert.NotEqual(0, new PEHeaders(stream).CorHeader.EntryPointTokenOrRelativeVirtualAddress);

stream.Position = 0;
var reference = AssemblyMetadata.CreateFromStream(stream).GetReference();
var comp = CreateCompilation("", references: [reference],
options: TestOptions.DebugDll.WithMetadataImportOptions(MetadataImportOptions.All));
var main = comp.GetMember<MethodSymbol>("Program.<Main>");
Assert.Equal(Accessibility.Private, main.DeclaredAccessibility);
}
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/76707")]
public void ExcludePrivateMembers_DebugEntryPoint()
{
using var peStream = new MemoryStream();
using var metadataStream = new MemoryStream();

{
var comp = CreateCompilation("""
static class Program
{
static void M1() { }
static void M2() { }
}
""").VerifyDiagnostics();
var emitResult = comp.Emit(
peStream: peStream,
metadataPEStream: metadataStream,
debugEntryPoint: comp.GetMember<MethodSymbol>("Program.M1").GetPublicSymbol(),
options: EmitOptions.Default.WithIncludePrivateMembers(false));
Assert.True(emitResult.Success);
emitResult.Diagnostics.Verify();
}

{
// M1 should be emitted (it's the debug entry-point), M2 shouldn't (private members are excluded).
metadataStream.Position = 0;
var reference = AssemblyMetadata.CreateFromStream(metadataStream).GetReference();
var comp = CreateCompilation("", references: [reference],
options: TestOptions.DebugDll.WithMetadataImportOptions(MetadataImportOptions.All));
var m1 = comp.GetMember<MethodSymbol>("Program.M1");
Assert.Equal(Accessibility.Private, m1.DeclaredAccessibility);
Assert.Null(comp.GetMember<MethodSymbol>("Program.M2"));
}
}
}
}
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4879,12 +4879,14 @@ class C { }
public void AttributeArgumentAsEnumFromMetadata()
{
var metadataStream1 = CSharpCompilation.Create("bar.dll",
options: TestOptions.DebugDll,
references: new[] { MscorlibRef },
syntaxTrees: new[] { Parse("public enum Bar { Baz }") }).EmitToStream(options: new EmitOptions(metadataOnly: true));

var ref1 = MetadataReference.CreateFromStream(metadataStream1);

var metadataStream2 = CSharpCompilation.Create("goo.dll", references: new[] { MscorlibRef, ref1 },
options: TestOptions.DebugDll,
syntaxTrees: new[] {
SyntaxFactory.ParseSyntaxTree(
"public class Ca : System.Attribute { public Ca(object o) { } } " +
Expand Down
6 changes: 4 additions & 2 deletions src/Compilers/CSharp/Test/Semantic/Semantics/LambdaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,8 @@ End Module
var vbProject = VisualBasic.VisualBasicCompilation.Create(
"VBProject",
references: new[] { MscorlibRef },
syntaxTrees: new[] { VisualBasic.VisualBasicSyntaxTree.ParseText(vbSource) });
syntaxTrees: new[] { VisualBasic.VisualBasicSyntaxTree.ParseText(vbSource) },
options: new VisualBasic.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var csSource = @"
class Program
Expand Down Expand Up @@ -557,7 +558,8 @@ End Module
var vbProject = VisualBasic.VisualBasicCompilation.Create(
"VBProject",
references: new[] { MscorlibRef },
syntaxTrees: new[] { VisualBasic.VisualBasicSyntaxTree.ParseText(vbSource) });
syntaxTrees: new[] { VisualBasic.VisualBasicSyntaxTree.ParseText(vbSource) },
options: new VisualBasic.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var csSource = @"
class Program
Expand Down
5 changes: 5 additions & 0 deletions src/Compilers/Core/Portable/PEWriter/Members.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,11 @@ public static bool ShouldInclude(this ITypeDefinitionMember member, EmitContext
}
}

if (method != null && (context.Module.PEEntryPoint == method || context.Module.DebugEntryPoint == method))
{
return true;
}

return false;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1869,7 +1869,7 @@ public PortablePdbBuilder GetPortablePdbBuilder(ImmutableArray<int> typeSystemRo

internal void GetEntryPoints(out MethodDefinitionHandle entryPointHandle, out MethodDefinitionHandle debugEntryPointHandle)
{
if (IsFullMetadata && !MetadataOnly)
if (IsFullMetadata)
{
// PE entry point is set for executable programs
IMethodReference entryPoint = module.PEEntryPoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2487,6 +2487,16 @@ Namespace Microsoft.CodeAnalysis.VisualBasic
End If

SynthesizedMetadataCompiler.ProcessSynthesizedMembers(Me, moduleBeingBuilt, cancellationToken)

If moduleBeingBuilt.OutputKind.IsApplication() Then
Dim entryPoint = GetEntryPointAndDiagnostics(cancellationToken)
diagnostics.AddRange(entryPoint.Diagnostics)
If entryPoint.MethodSymbol IsNot Nothing AndAlso Not entryPoint.Diagnostics.HasAnyErrors() Then
moduleBeingBuilt.SetPEEntryPoint(entryPoint.MethodSymbol, diagnostics)
Else
Return False
End If
End If
Else
' start generating PDB checksums if we need to emit PDBs
If (emittingPdb OrElse moduleBuilder.EmitOptions.InstrumentationKinds.Contains(InstrumentationKind.TestCoverage)) AndAlso
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1875,13 +1875,15 @@ End Class
<Fact>
Public Sub AttributeArgumentAsEnumFromMetadata()
Dim metadata1 = VisualBasicCompilation.Create("bar.dll",
options:=TestOptions.DebugDll,
references:={MscorlibRef},
syntaxTrees:={Parse("Public Enum Bar : Baz : End Enum")}).EmitToArray(New EmitOptions(metadataOnly:=True))

Dim ref1 = MetadataReference.CreateFromImage(metadata1)

Dim metadata2 = VisualBasicCompilation.Create(
"goo.dll",
options:=TestOptions.DebugDll,
references:={MscorlibRef, ref1},
syntaxTrees:={
VisualBasicSyntaxTree.ParseText(<![CDATA[
Expand Down
Loading

0 comments on commit c46ba8b

Please sign in to comment.