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

Wpf applications can't be published as a trimmed single exe #651

Closed
sbomer opened this issue Jul 9, 2019 · 18 comments
Closed

Wpf applications can't be published as a trimmed single exe #651

sbomer opened this issue Jul 9, 2019 · 18 comments

Comments

@sbomer
Copy link
Member

sbomer commented Jul 9, 2019

https://github.com/dotnet/coreclr/issues/25334#issue-459511455 from @Lakritzator

I'm not sure if this is the correct repository for this.

When using dotnet sdk 3.0.100-preview7-012564 (and a lot of builds before this) I get a crash when starting a publishing a WPF application as a trimmed single exe.

An example repository is located here: https://github.com/Lakritzator/WpfPublishTrimmedCrash
It's a "dotnet new wpf" with a textbox added, as without it the error is not visible.

Running the following on the project:

dotnet publish WpfTest.csproj -f netcoreapp3.0 -c Release /p:PublishSingleFile=true /p:PublishTrimmed=true /p:RuntimeIdentifier=win-x64

Than starting the WpfText.exe which is located in bin\Release\netcoreapp3.0\win-x64\publish
will not start the application, and in the eventlog I can see the following error:

Application: WpfTest.exe
CoreCLR Version: 4.700.19.32201
.NET Core Version: 3.0.0-preview7-27822-04
Description: The process was terminated due to an unhandled exception.
Exception Info: System.TypeInitializationException: The type initializer for 'System.Windows.Media.FontFamily' threw an exception.
 ---> System.TypeInitializationException: The type initializer for 'MS.Internal.FontCache.DWriteFactory' threw an exception.
 ---> System.InvalidCastException: Specified cast is not valid.
   at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode, IntPtr errorInfo)
   at MS.Internal.Text.TextInterface.Native.Util.ConvertHresultToException(Int32 hr)
   at MS.Internal.Text.TextInterface.Factory.Initialize(FactoryType factoryType)
   at MS.Internal.Text.TextInterface.Factory..ctor(FactoryType factoryType, IFontSourceCollectionFactory fontSourceCollectionFactory, IFontSourceFactory fontSourceFactory)
   at MS.Internal.Text.TextInterface.Factory.Create(FactoryType factoryType, IFontSourceCollectionFactory fontSourceCollectionFactory, IFontSourceFactory fontSourceFactory)
   at MS.Internal.FontCache.DWriteFactory..cctor()
   --- End of inner exception stack trace ---
   at MS.Internal.FontCache.DWriteFactory.get_SystemFontCollection()
   at MS.Internal.FontCache.FamilyCollection.FromWindowsFonts(Uri folderUri)
   at System.Windows.Media.FontFamily.PreCreateDefaultFamilyCollection()
   at System.Windows.Media.FontFamily..cctor()
   --- End of inner exception stack trace ---
   at System.Windows.Media.Typeface..ctor(FontFamily fontFamily, FontStyle style, FontWeight weight, FontStretch stretch)
   at MS.Internal.Text.DynamicPropertyReader.GetTypeface(DependencyObject element)
   at MS.Internal.Text.TextProperties.InitCommon(DependencyObject target)
   at MS.Internal.Text.TextProperties..ctor(FrameworkElement target, Boolean isTypographyDefaultValue)
   at System.Windows.Controls.TextBoxView.GetLineProperties()
   at System.Windows.Controls.TextBoxView.TextCache..ctor(TextBoxView owner)
   at System.Windows.Controls.TextBoxView.EnsureCache()
   at System.Windows.Controls.TextBoxView.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
   at System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
   at System.Windows.Controls.ScrollContentPresenter.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV)
   at System.Windows.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV, Boolean& hasDesiredSizeUChanged)
   at System.Windows.Controls.Grid.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.ScrollViewer.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Border.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Control.MeasureOverride(Size constraint)
   at System.Windows.Controls.TextBox.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.StackPanel.StackMeasureHelper(IStackMeasure measureElement, IStackMeasureScrollData scrollData, Size constraint)
   at System.Windows.Controls.StackPanel.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
   at System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Decorator.MeasureOverride(Size constraint)
   at System.Windows.Documents.AdornerDecorator.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Border.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Window.MeasureOverrideHelper(Size constraint)
   at System.Windows.Window.MeasureOverride(Size availableSize)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Interop.HwndSource.SetLayoutSize()
   at System.Windows.Interop.HwndSource.set_RootVisualInternal(Visual value)
   at System.Windows.Interop.HwndSource.set_RootVisual(Visual value)
   at System.Windows.Window.SourceWindowHelper.set_RootVisual(Visual value)
   at System.Windows.Window.SetRootVisual()
   at System.Windows.Window.SetRootVisualAndUpdateSTC()
   at System.Windows.Window.SetupInitialState(Double requestedTop, Double requestedLeft, Double requestedWidth, Double requestedHeight)
   at System.Windows.Window.CreateSourceWindow(Boolean duringShow)
   at System.Windows.Window.CreateSourceWindowDuringShow()
   at System.Windows.Window.SafeCreateWindowDuringShow()
   at System.Windows.Window.ShowHelper(Object booleanBox)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.WrappedInvoke(Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at System.Windows.Threading.Dispatcher.Invoke(DispatcherPriority priority, Delegate method, Object arg)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at WpfTest.App.Main()

https://github.com/dotnet/coreclr/issues/25334#issuecomment-505107221 from @jeffschwMSFT

@Lakritzator thanks for reporting this. Within the .NET Core 3 timeframe there is going to be developer involvement to get most apps trimmed via the linker. We are striving to find the right defaults, and having your example will help us. Thanks

https://github.com/dotnet/coreclr/issues/25334#issuecomment-506836765 from @sbomer

For this specific case, I found two things:

System.Diagnostics.Debug is used at runtime. Adding this to TrimmerRootAssembly gets halfway there, but the app still crashes.
There is a problem with DirectWriteForwarder.dll. If you copy an unlinked DirectWriteForwarder.dll into the linked app, it works. If I ildasm and then ilasm this dll even without using the linker, I get the same problem. Maybe some C++/CLI data is being lost in the ilasm round trip, though it's compiled with /clr:pure, so I expect it to just be a managed assembly. It could be related to dotnet/wpf#634, in which case this would be a C++/CLI compiler bug. I'll keep looking into it.
@vatsan-madhavan
Copy link
Member

/cc @tgani-msft, @rladuca

@jkoritzinsky
Copy link
Member

I found the root cause:

The C++ compiler initializes static variables via data table in the metadata. Specifically, it generates .data entries in the image containing the metadata tokens of the static initializer functions with a 00 00 00 00 entry before and a 00 00 00 00 entry afterwards. There are fields (__xc_ma_a and __xc_ma_z) that point to these ends. When the assembly is loaded into the runtime, the assembly's module initializer loops through __xc_ma_a to __xc_ma_z, resolves the method tokens, and calls the initializer method via a calli.

The IL Linker reorders the .data entries in the IL image when trimming. As a result, variables whose initializer token entry is not between __xc_ma_a and __xc_ma_z never get initialized.

Native vtables have the same issue since they use the same initialization mechanism (with __xi_vt_a and __xi_vt_z as the variables at the ends).

To support C++/CLI images, the linker needs to preserve the ordering of the .data entries in the image.

@jeffschwMSFT
Copy link
Member

@sbomer do we know if the illinker can reliably rewrite all C++/CLI images? It may be safer to explicitly exclude such assemblies from rewriting (which would avoid this particular issue).

@sbomer
Copy link
Member Author

sbomer commented Jul 12, 2019

I don't believe so - I agree that we should exclude them. @vatsan-madhavan /@jkoritzinsky is there a reliable way to distinguish C++/CLI images from normal managed dlls? If not we may need to special-case DirectWriteForwarder.

@vatsan-madhavan
Copy link
Member

vatsan-madhavan commented Jul 12, 2019

You can reliably identify WPF's C++/CLI images by looking for [assembly: AssemblyMetadata("Language", "C++")]. We added it anticipating just this sort of situation ;-)

I don't know of a way to reliably identify every C++/CLI image.

WPF has two assemblies that are C++/CLI: DirectWriteForwarder and System.Printing. Using the AssemblyMetadataAttribute to identify these leaves us free to rename/refactor etc. in the future without creating a tight-coupling between linker and WPF.

@jeffschwMSFT
Copy link
Member

The corflags (part of the COM_20 header) for C++/CLI images are 0x0... though even IL only images can also be 0x0 (though not when produced with our tooling).

@vatsan-madhavan
Copy link
Member

At present, WPF is the only real user of C++/CLI on .NET Core. When official support for C++/CLI is made available in Dev16.3 timeframe, we should either have linker support C++/CLI (which would amount to mixed-mode image support - likely a stretch-goal), or have the props/targets support in Visual Studio automatically stamp AssemblyMetadata(Language,C++) or something similar on C++ images so that you can keep on excluding customer-generated C++/CLI images as well.

@jkoritzinsky
Copy link
Member

I don't know of any full proof way, but there's a few things that you could use as reliable heuristics.

  • Mixed-mode assemblies will not have the flags part of the CLR header set to IL Library.
  • C++/CLI (mixed-mode and pure) assemblies will have .module extern entries in their manifest (as shown by ildasm).
  • All C++/CLI assemblies will have a <CrtImplementationDetails> namespace and a <CppImplementationDetails> namespace.

@vatsan-madhavan
Copy link
Member

vatsan-madhavan commented Jul 12, 2019

The C++/CLI assemblies produced by WPF at present are /clr:pure. This is what I see, which seems about right.

λ corflags "C:\Program Files (x86)\dotnet\shared\Microsoft.WindowsDesktop.App\3.0.0-preview8-27910-05\System.Printing.dll"
Microsoft (R) .NET Framework CorFlags Conversion Tool.  Version  4.6.1055.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 0x6
ILONLY    : 0
32BITREQ  : 1
32BITPREF : 0
Signed    : 0

λ corflags "C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\3.0.0-preview8-27910-05\System.Printing.dll"
Microsoft (R) .NET Framework CorFlags Conversion Tool.  Version  4.6.1055.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 0x4
ILONLY    : 0
32BITREQ  : 0
32BITPREF : 0
Signed    : 0

Not sure why ILLibrary is set on these instead of ILOnly... could it be the result of crossgen?

@jkoritzinsky
Copy link
Member

Based on @vatsan-madhavan's response, you can also use the ILOnly flag as the heuristic since it seems that the C++/CLI compiler never sets the ILOnly flag (it definitely isn't set for mixed-mode).

@vatsan-madhavan
Copy link
Member

There is something odd going on.

Here is what I see in my local build for a 32-bit image:

λ corflags artifacts\packaging\Release\Microsoft.DotNet.Wpf.GitHub\lib\netcoreapp3.0\DirectWriteForwarder.dll
Microsoft (R) .NET Framework CorFlags Conversion Tool.  Version  4.6.1055.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 0x3
ILONLY    : 1
32BITREQ  : 1
32BITPREF : 0
Signed    : 1

The compiler set ILOnly in this case. I suspect that crossgen changed CorFlags after the fact....

@jkoritzinsky
Copy link
Member

Yeah now that you mention it I think ReadyToRun does that. Nevermind my previous comment about using that as a heuristic.

@sbomer
Copy link
Member Author

sbomer commented Jul 12, 2019

I'm seeing that (non-C++/CLI) R2R assemblies (which the linker can process) are ILOnly = 0, ILLibrary = 1.

@vatsan-madhavan
Copy link
Member

Yeah now that you mention it I think ReadyToRun does that. Nevermind my previous comment about using that as a heuristic.

Verified local build output for x64 also - ILOnly is definitely being set by the compiler.

λ corflags artifacts\packaging\Release\x64\Microsoft.DotNet.Wpf.GitHub\lib\netcoreapp3.0\DirectWriteForwarder.dll
Microsoft (R) .NET Framework CorFlags Conversion Tool.  Version  4.6.1055.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 0x1
ILONLY    : 1
32BITREQ  : 0
32BITPREF : 0
Signed    : 1

@sbomer
Copy link
Member Author

sbomer commented Jul 12, 2019

So, crossgen'd WPF C++/CLI (clr/pure) are indistinguishable from crossgen'd .NET assemblies on the basis of ILOnly/ILLibrary, right? Does crossgen support mixed-mode assemblies?

I'm also seeing some .NET assemblies with .module extern. So it sounds like excluding assemblies with <CrtImplementationDetails> or <CppImplementationDetails> will do what we want (exclude any C++/CLI assemblies, user or WPF, mixed or pure). Does that sound good to you @vatsan-madhavan?

@vatsan-madhavan
Copy link
Member

So it sounds like excluding assemblies with <CrtImplementationDetails> or <CppImplementationDetails> will do what we want (exclude any C++/CLI assemblies, user or WPF, mixed or pure).

Sounds like a reasonable plan to me. An authoritative answer is best obtained from @tgani-msft (better asked over email) - esp. if the plan is to leverage <CppImplementationDetails> as a sort of contract that would require the compiler to, say, never remove this namespace.

@sbomer
Copy link
Member Author

sbomer commented Jul 13, 2019

I tested this change (#658 - note the caveats mentioned in this PR) and this fixes the immediate issue. However the app still doesn't start unless System.Diagnostics.Debug is added as a root. The app hits the following exception:

Could not load type 'System.Runtime.InteropServices.Marshal' from assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.

It seems that the inclusion of the debug dll lets it recover from the exception. The exception looks like it's caused by dotnet/wpf#634. greenshot/greenshot#135 (comment) suggests a fix is in the works for this - I will try this again when that's available.

vatsan-madhavan added a commit to dotnet/wpf that referenced this issue Jul 25, 2019
…C++/CLI images. See dotnet/linker#651 and dotnet/linker#658.

In turn, this results in a failure of dependencies of such assemblies (like System.Diagnostics.Debug.dll, which is required by DirectWriteForwarder.dll) from being identified and included in the ReadyToRun images.

These linker hints tell ILLinker to include certain dependencies, which we know to be required by DirectWriteForwarder and System.Printing respectively.
ghost pushed a commit to dotnet/wpf that referenced this issue Jul 26, 2019
* When producing ReadyToRun images, the ILLinker is configured to skip C++/CLI images. See dotnet/linker#651 and dotnet/linker#658.

In turn, this results in a failure of dependencies of such assemblies (like System.Diagnostics.Debug.dll, which is required by DirectWriteForwarder.dll) from being identified and included in the ReadyToRun images.

These linker hints tell ILLinker to include certain dependencies, which we know to be required by DirectWriteForwarder and System.Printing respectively.

* Add netstandard and mscorlib to the list of dependencies
@marek-safar
Copy link
Contributor

Closing as the workaround has landed as well as the fix for System.Diagnostics.Debug. The support for C++ assemblies is tracked at #676

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants