Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/coreclr/hosts/corerun/corerun.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ static int run(const configuration& config)
argv_utf8.get(),
entry_assembly_utf8.c_str(),
(uint32_t*)&exit_code);

if (FAILED(result))
{
pal::fprintf(stderr, W("BEGIN: coreclr_execute_assembly failed - Error: 0x%08x\n"), result);
Expand Down
73 changes: 69 additions & 4 deletions src/coreclr/vm/assembly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@ void RunManagedStartup()
managedStartup.Call(args);
}

INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
void Assembly::ExecuteMainMethodPre(BOOL waitForOtherThreads)
{
CONTRACTL
{
Expand All @@ -1397,9 +1397,6 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre
// reset the error code for std C
errno=0;

HRESULT hr = S_OK;
INT32 iRetVal = 0;

Thread *pThread = GetThread();
MethodDesc *pMeth;
{
Expand Down Expand Up @@ -1446,6 +1443,74 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre

RunManagedStartup();

Thread::CleanUpForManagedThreadInNative(pThread);
}
}

//RunMainPost is supposed to be called on the main thread of an EXE,
//after that thread has finished doing useful work. It contains logic
//to decide when the process should get torn down. So, don't call it from
// AppDomain.ExecuteAssembly()
if (pMeth) {
if (waitForOtherThreads)
RunMainPost();
}
else {
StackSString displayName;
GetDisplayName(displayName);
COMPlusThrowHR(COR_E_MISSINGMETHOD, IDS_EE_FAILED_TO_FIND_MAIN, displayName);
}

}

INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you trying to achieve by this refactoring?

Copy link
Member

@steveisok steveisok Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What we are trying to determine is the best place to allow swapping the entry assembly and it having a real effect. The goal in #99883 is to allow some / all of the TPA assemblies to load, sleep/wake up, and allow the entry assembly to be swapped.

We've identified two ways to do this. Make the runtime aware, which is what this PR is attempting to do. And the second is another export in the host that doesn't shut the runtime down after it's done (the onus would be put on the custom host to do that).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative approach we've discussed in the past is this:

  • Rely on startup hook to "stop" the runtime and wait for knowing the final entry point assembly
  • Add a new API which sets the entry point assembly for all of the other places where the runtime uses this information
  • Rely on reflection to load an execute the entry point assembly from the startup hook.
  • Basically the original entry point assembly would never be executed.

This is more of the "do this in managed code" approach. Not sure how this would fit with the host changes to allow for TPA modifications after startup.

There are other similar ways - for example the above could be done without startup hooks, where the original Main would perform the operations and effectively "replace itself".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Rely on reflection to load an execute the entry point assembly from the startup hook.
  • Basically the original entry point assembly would never be executed.

The way the runtime appears to work right now is that the original main will be executed regardless of what you do in the hook. Also, aren't there limitations executing code from the hook?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the runtime appears to work right now is that the original main will be executed regardless of what you do in the hook.

Yes - but nothing is stopping the hook from calling Environment.Exit ;-)
I think the idea of doing this from the main is better anyway. That's what dotnet watch uses for ASP.NET anyway. BTW that would be another interesting thing to look at - they're one of the teams asking for the entry point replacement as the watch breaks some apps which rely on it.

Also, aren't there limitations executing code from the hook?

Not really - AFAIK there are no restrictions other than "you should not do that" - but mostly those are there to not break the "Main", but if the "Main" is empty then they're moot.

Copy link
Member

@jkotas jkotas Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this would fit with the host changes to allow for TPA modifications after startup.

TPA additions after startup can be done via the AssemblyLoadContext.Default.

I do not think we should be enabling modifications of TPA after startup. It would be a pit of failure when an assembly that wanted to be overridden by the copy in the app got loaded too early. Instead, we should figure out how to startup the runtime with TPA subset that is never going to be modified.

Copy link
Member Author

@fanyang-mono fanyang-mono Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we take the Main approach, then the real app main could be invoked by reflection. I wonder what is the need of creating a new API Assembly.SetEntryPoint? It seems that what Functions team would like to do could be achieved with existing API's. Am I missing anything?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the new API for example this Assembly.GetEntryAssembly would return wrong value (it would point to the "host's template Main"). I think there are other places that this shows up in some way or form as well. Some apps use this to mostly get to the location of app's files (Assembly.GetEntryAssembly().Location) for example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASP.NET Core has number of places that call Assembly.GetEntryAssembly: https://github.com/search?q=repo%3Adotnet%2Faspnetcore+GetEntryAssembly&type=code

As Vitek said, it is important for Assembly.GetEntryAssembly to return the actual application assembly to avoid breaking these places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha!

{
CONTRACTL
{
INSTANCE_CHECK;
THROWS;
GC_TRIGGERS;
MODE_ANY;
ENTRY_POINT;
INJECT_FAULT(COMPlusThrowOM());
}
CONTRACTL_END;

// reset the error code for std C
errno=0;

HRESULT hr = S_OK;
INT32 iRetVal = 0;

Thread *pThread = GetThread();
MethodDesc *pMeth;
{
// This thread looks like it wandered in -- but actually we rely on it to keep the process alive.
pThread->SetBackground(FALSE);

GCX_COOP();

pMeth = GetEntryPoint();

if (pMeth) {
{
#ifdef FEATURE_COMINTEROP
GCX_PREEMP();

Thread::ApartmentState state = Thread::AS_Unknown;
state = SystemDomain::GetEntryPointThreadAptState(pMeth->GetMDImport(), pMeth->GetMemberDef());
SystemDomain::SetThreadAptState(state);
#endif // FEATURE_COMINTEROP
}

RunMainPre();

// Perform additional managed thread initialization.
// This would is normally done in the runtime when a managed
// thread is started, but is done here instead since the
// Main thread wasn't started by the runtime.
Thread::InitializationForManagedThreadInNative(pThread);

hr = RunMain(pMeth, 1, &iRetVal, stringArgs);

Thread::CleanUpForManagedThreadInNative(pThread);
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/assembly.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class Assembly

//****************************************************************************************
//
void ExecuteMainMethodPre(BOOL waitForOtherThreads);
INT32 ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads);

//****************************************************************************************
Expand Down
13 changes: 13 additions & 0 deletions src/coreclr/vm/assemblynative.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,19 @@ extern "C" void QCALLTYPE AssemblyNative_GetEntryAssembly(QCall::ObjectHandleOnS
END_QCALL;
}

extern "C" void QCALLTYPE AssemblyNative_UpdateEntryAssembly(QCall::AssemblyHandle assemblyHandle)
{
QCALL_CONTRACT;

BEGIN_QCALL;

Assembly* pAssembly = assemblyHandle->GetAssembly();
PTR_AppDomain pCurDomain = GetAppDomain();
pCurDomain->SetRootAssembly(pAssembly);

END_QCALL;
}

extern "C" void QCALLTYPE AssemblyNative_GetImageRuntimeVersion(QCall::AssemblyHandle pAssembly, QCall::StringHandleOnStack retString)
{
QCALL_CONTRACT;
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/vm/assemblynative.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ extern "C" uint32_t QCALLTYPE AssemblyNative_GetAssemblyCount();

extern "C" void QCALLTYPE AssemblyNative_GetEntryAssembly(QCall::ObjectHandleOnStack retAssembly);

extern "C" void QCALLTYPE AssemblyNative_UpdateEntryAssembly(QCall::AssemblyHandle assemblyHandle);


extern "C" void QCALLTYPE AssemblyNative_GetExecutingAssembly(QCall::StackCrawlMarkHandle stackMark, QCall::ObjectHandleOnStack retAssembly);

Expand Down
10 changes: 8 additions & 2 deletions src/coreclr/vm/corhost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,10 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId,
if(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_Corhost_Swallow_Uncaught_Exceptions))
{
EX_TRY
DWORD retval = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
pAssembly->ExecuteMainMethodPre(TRUE /* waitForOtherThreads */);
AppDomain *curDomain = SystemDomain::GetCurrentDomain();
Assembly *curEntryAssembly = curDomain->GetRootAssembly();
DWORD retval = curEntryAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
if (pReturnValue)
{
*pReturnValue = retval;
Expand All @@ -346,7 +349,10 @@ HRESULT CorHost2::ExecuteAssembly(DWORD dwAppDomainId,
}
else
{
DWORD retval = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
pAssembly->ExecuteMainMethodPre(TRUE /* waitForOtherThreads */);
AppDomain *curDomain = SystemDomain::GetCurrentDomain();
Assembly *curEntryAssembly = curDomain->GetRootAssembly();
DWORD retval = curEntryAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
if (pReturnValue)
{
*pReturnValue = retval;
Expand Down
61 changes: 61 additions & 0 deletions src/mono/sample/HelloWorld_2/HelloWorld_2.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
<!-- always set SelfContained when running to use Mono on desktop -->
<SelfContained>true</SelfContained>
</PropertyGroup>

<UsingTask TaskName="MonoAOTCompiler"
AssemblyFile="$(MonoAOTCompilerTasksAssemblyPath)" />

<Target Name="AOTCompileApp" Condition="'$(RunAOTCompilation)' == 'true'" AfterTargets="CopyFilesToPublishDirectory">
<PropertyGroup>
<_AotOutputType>Library</_AotOutputType>
<_AotLibraryFormat>Dylib</_AotLibraryFormat>
<UseAotDataFile>false</UseAotDataFile>
</PropertyGroup>

<ItemGroup>
<AotInputAssemblies Include="$(PublishDir)\*.dll" />
</ItemGroup>

<MonoAOTCompiler
CompilerBinaryPath="@(MonoAotCrossCompiler->WithMetadataValue('RuntimeIdentifier','$(TargetOS)-$(TargetArchitecture.ToLowerInvariant())'))"
OutputType="$(_AotOutputType)"
LibraryFormat="$(_AotLibraryFormat)"
Assemblies="@(AotInputAssemblies)"
OutputDir="$(PublishDir)"
CollectTrimmingEligibleMethods="$(StripILCode)"
TrimmingEligibleMethodsOutputDirectory="$(TrimmingEligibleMethodsOutputDirectory)"
IntermediateOutputPath="$(IntermediateOutputPath)"
UseAotDataFile="$(UseAotDataFile)"
MibcProfilePath="$(MibcProfilePath)"
UseLLVM="$(MonoEnableLLVM)"
LLVMPath="$(MonoAotCrossDir)">
<Output TaskParameter="CompiledAssemblies" ItemName="BundleAssemblies" />
</MonoAOTCompiler>
</Target>

<UsingTask TaskName="ILStrip"
AssemblyFile="$(MonoTargetsTasksAssemblyPath)" />

<Target Name="StripILCode" Condition="'$(StripILCode)' == 'true'" AfterTargets="AOTCompileApp">
<PropertyGroup>
<TrimIndividualMethods>true</TrimIndividualMethods>
</PropertyGroup>

<ILStrip
TrimIndividualMethods="$(TrimIndividualMethods)"
Assemblies="@(BundleAssemblies)">
<Output TaskParameter="TrimmedAssemblies" ItemName="TrimmedAssemblies" />
</ILStrip>

<Copy
SourceFiles="@(TrimmedAssemblies->Metadata('TrimmedAssemblyFileName'))"
DestinationFiles="@(TrimmedAssemblies)"
OverwriteReadOnlyFiles="true"
/>
<Delete Files="@(TrimmedAssemblies->Metadata('TrimmedAssemblyFileName'))" />
</Target>
</Project>
34 changes: 34 additions & 0 deletions src/mono/sample/HelloWorld_2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
TOP=../../../../
DOTNET:=$(TOP)dotnet.sh
DOTNET_Q_ARGS=--nologo -v:q -consoleloggerparameters:NoSummary

MONO_CONFIG?=Debug
MONO_ARCH?=$(shell . $(TOP)eng/common/native/init-os-and-arch.sh && echo $${arch})
TARGET_OS?=$(shell . $(TOP)eng/common/native/init-os-and-arch.sh && echo $${os})
AOT?=false
USE_LLVM?=false
StripILCode?=false
TrimmingEligibleMethodsOutputDirectory?= #<path-to-a-writable-directory>

#MIBC_PROFILE_PATH=<path-to-mibc-for-sample>

MONO_ENV_OPTIONS ?=

publish:
$(DOTNET) publish \
-c $(MONO_CONFIG) \
-r $(TARGET_OS)-$(MONO_ARCH) \
/p:RunAOTCompilation=$(AOT) \
/p:MonoEnableLLVM=$(USE_LLVM) \
/p:StripILCode=$(StripILCode) \
/p:TrimmingEligibleMethodsOutputDirectory=$(TrimmingEligibleMethodsOutputDirectory) \
'/p:MibcProfilePath="$(MIBC_PROFILE_PATH)"' \
/bl

run: publish
DOTNET_DebugWriteToStdErr=1 \
MONO_ENV_OPTIONS="$(MONO_ENV_OPTIONS)" \
$(TOP)artifacts/bin/HelloWorld_2/$(MONO_ARCH)/$(MONO_CONFIG)/$(TARGET_OS)-$(MONO_ARCH)/publish/HelloWorld_2

clean:
rm -rf $(TOP)artifacts/bin/HelloWorld_2/
15 changes: 15 additions & 0 deletions src/mono/sample/HelloWorld_2/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace HelloWorld_2
{
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine($"Hello World 2");
}
}
}