-
Notifications
You must be signed in to change notification settings - Fork 1k
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
fix: PrintDialog
not shown
#2851
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2851 +/- ##
============================================
Coverage ? 60.24856%
============================================
Files ? 1248
Lines ? 433383
Branches ? 38850
============================================
Hits ? 261107
Misses ? 166913
Partials ? 5363
|
src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/NativeMethods.cs
Outdated
Show resolved
Hide resolved
public ushort nCopies; | ||
public IntPtr hInstance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By no longer having separate structs for 32bit/64bit you are breaking the layout here, 32 bit will add 2 byte padding here where none is expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, original source also had this in comments/attributes, 32bit uses Pack=1, 64bit uses default packing. Since the headers explicitely switch packing depending on bitness you probably can't avoid having two layouts, at least I can't think of any single definition compatible with both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could have #if WIN32 or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#if
is a compile-time directive, I don't think you want to compile different C# assemblies for 32/64 bit do you? Usually you only have one anycpu assembly for .NET
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for pointing this one out. Very insightful.
I have built an x86 variant, and sure enough, it didn't work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked internally and there is no desire to build leaf-node stacks per arch, as it is very involved. The binaries get cross-gened for each setup bundle.
This means we must have two different versions of the struct (due to different packing requirements), and jump through hoops to populate the payload and invoke the underlying Win32 API.
src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/NativeMethods.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/System/Windows/Forms/Internals/NativeMethods.cs
Outdated
Show resolved
Hide resolved
Thank you for the reviews. This was a quick EOD POC to see it fixed. More cleanups are in order, as you rightly pointed. |
3c40954
to
8dc86e3
Compare
data.hwndOwner = hwndOwner; | ||
data.lpfnPrintHook = hookProcPtr; | ||
data.lpfnPrintHook = Marshal.GetFunctionPointerForDelegate(new User32.WNDPROCINT(HookProc)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a more optimal way of doing this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nothing I'm aware of - you could cache the function pointer in a static variable if the hook were a static method, but then you also need to cache the delegate to prevent it being collected. Overall I don't think its worth it, showing print dialogs isn't exactly a hot loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually I'm wondering if this needs a GC.KeepAlive on the delegate. There is other interop code running before the dialog is shown so there could be a small risk the GC collects the delegate before entering native code which uses the hook?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't get it to crash by inserting explicit GC.Collect in the code, so its probably fine, but I'm not sure why as the documentation explicitly says
You must manually keep the delegate from being collected by the garbage collector from managed code. The garbage collector does not track references to unmanaged code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you need to GC.KeepAlive
the new
object. One gotcha that got me many years ago is that collection behavior is different in debug versus release. When running a debug build your locals are scoped to the end of the frame (the method or it's parent method if it was inlined). I don't know precisely how the heuristics work and if there are any differences in Core. I'll try and make a point of finding out exactly how the frame GC reporting works, but depending on the implementation isn't the way to go. We need to go by the specification, which is as soon as you're no longer directly referencing a managed heap pointer, it's eligible for collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
src/System.Windows.Forms/tests/IntegrationTests/WinformsControlsTest/MainForm.Designer.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/tests/Interop/Comdlg32/PRINTDLGWTests.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/tests/Interop/Comdlg32/PRINTDLGWTests.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/tests/Interop/Comdlg32/PRINTDLGWTests.cs
Outdated
Show resolved
Hide resolved
[ConditionalFact(nameof(Is64bit))] | ||
public unsafe void PRINTDLGW_64_Size() | ||
{ | ||
Assert.Equal(120, Marshal.SizeOf<PRINTDLGW_64>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted here there are differences between blitting structs and classic marshalling. Your test cases are mixing up both styles so you test the size using classic marshalling and offsets using blitting layout. You may want to be consistent in what you test. (Though as far as I'm currently aware it can only differ if bool
is used somewhere.)
Marshal.SizeOf
for classic marshalling sizeMarshal.OffsetOf
for classic marshalling offsetssizeof
keyword for blitting size- pointer arithmetic for blitting offsets
Example where it makes a difference:
public struct Sample
{
public int a;
public bool b;
public short c;
}
var sample = new Sample();
var blittingSize = sizeof(Sample); // 8
var blittingOffset = (byte*)&sample.c - (byte*)&sample; // 6
var marshalSize = Marshal.SizeOf<Sample>(); // 12
var marshalOffset = Marshal.OffsetOf<Sample>(nameof(Sample.c)); // 8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you 👍
It started as a non-blittable struct and I think that's when I started writing tests, and forgot to switch. Fixed now.
src/System.Windows.Forms.Primitives/src/Interop/Comdlg32/Interop.PrintDlg.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/Comdlg32/Interop.PrintDlgW.cs
Show resolved
Hide resolved
Resolves dotnet#2814 The bug was caused by the conversion of a struct fields to auto-properties in dotnet#2594. This broke the interop binary layout. Rework the code to - correctly define x86 and x64 structs - make the structs blittable - add tests verifying the structs layouts
5fec9be
to
93ae845
Compare
@msftbot merge in 24 hours |
Resolves dotnet#2814 The bug was caused by the conversion of a struct fields to auto-properties in dotnet#2594. This broke the interop binary layout. Rework the code to - correctly define x86 and x64 structs - make the structs blittable - add tests verifying the structs layouts
Resolves #2814
Proposed changes
The bug was caused by the conversion of a struct fields to auto-properties in #2594. This broke the interop binary layout.
Rework the code to
Customer Impact
PrintDialog
is now shown as expectedRegression?
Test methodology
manual
Microsoft Reviewers: Open in CodeFlow