Skip to content

Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime#223

Merged
jkoritzinsky merged 14 commits into
SharpGenTools:mainfrom
Sewer56:trimming-com
Jan 14, 2023
Merged

Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime#223
jkoritzinsky merged 14 commits into
SharpGenTools:mainfrom
Sewer56:trimming-com

Conversation

@Sewer56

@Sewer56 Sewer56 commented Jun 26, 2022

Copy link
Copy Markdown
Contributor

This PR picks up the work from #222.

Changes

  • Marked SharpGen.Runtime.COM as trimmable.

    • I've looked through the library thoroughly, it didn't have any code I believe to be linker unfriendly at all (no unfriendly reflection).
    • The parts dealing with COM Callable Wrapper equivalents (Shadows) were in SharpGen.Runtime itself.
    • I checked the source generated parts too, just in case.
  • Added a separate dummy trim tester to SharpGen.Runtime.COM in order to keep build system simple.

  • Extended SharpGen.Generator to automatically preserve interfaces of types inheriting from CallbackBase.

    • Should hopefully prevent COM interfaces passed to native code from being trimmed away.
    • Applies recursively (i.e. preserve type that inherits from type that inherits from CallbackBase.)
    • This is done by adding calls to a no-op function TrimmingHelpers.PreserveMe<T> to the module initializer.

No changes to existing public API.

Notes

  • *Variant.Value in SharpGen.Runtime.COM does make use of Reflection technically, GetType and check if type Is Primitive, however this has no effect on linking since the type just needs to exist for that to be resolved in the first place; and since it's passed into the method, it exists.

@Sewer56

Sewer56 commented Jun 26, 2022

Copy link
Copy Markdown
Contributor Author

So we want to try preventing trimming of code that may be called from native code.

I'd actually like to hear opinions about that; I've only really seen these used with COM in the context of SharpGenTools in the past; and COM is ultimately about methods only and everything ICallbackable in the library seems to revolve around COM.

However, it might be safer to enforce the whole types are preserved in the potential future scenario of ICallbackable(s) being used outside of COM, since we really can't predict native code.


Seems I'll also need to do something about SharpGen.Runtime.Trim.Dummy being built in CI; since that requires the environment for building SharpGen.Runtime.COM to be setup prior; so no merge until that's fixed. I'll do that later after a break.

@andrew-boyarshin

Copy link
Copy Markdown
Contributor

end users can't implement interfaces using non-public methods

What about explicit interface method implementations? They are non-public, as far as I remember.

@andrew-boyarshin

Copy link
Copy Markdown
Contributor

I have strong build infra concerns around SharpGen.Runtime.Trim.Dummy. SharpGenTools.Sdk and SharpGen.Runtime.COM can't be built in the same MSBuild invocation, but after this PR it will be (since your dummy project is in the same solution as SDK and references COM project). You have to remove dummy project from SDK & Runtime solution and add it to COM solution.

@andrew-boyarshin

Copy link
Copy Markdown
Contributor

Added DynamicallyAccessedMembers Interfaces to all code that is constrained to ICallbackable.

So, basically, to everything related to SharpGen and it's users. That's excessive, but I'm not sure if there is a way to preserve semantics. The semantics are: everything that is derived from CppObject can be trimmed safely in their entirety if not referenced (possibly through reflection though). Everything derived from CallbackBase has to always preserve all interface methods that derive from ICallbackable (technically, not true: currently it's all interfaces, since GUID list currently contains even runtime-generated type GUIDs, but that's stupid).

@Sewer56

Sewer56 commented Jun 27, 2022

Copy link
Copy Markdown
Contributor Author

end users can't implement interfaces using non-public methods

What about explicit interface method implementations? They are non-public, as far as I remember.

That's a really good point.
I'll see how the linker treats those with a dummy project when I get up later. I just woke up.

I also agree with moving the dummy to the COM solution. I was thinking about that part actually. Though I think it might be easier to have a separate dummy just for COM as far as development goes anyway, because then dealing with potential future analyzer warnings in main Runtime Lib would be pretty annoying.

As for the preserving the semantics, yeah, it'd be pretty difficult without breaking the existing public API. I'll comment more when I get working on this later.

@Sewer56 Sewer56 changed the title Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime [WIP] Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime Jun 27, 2022
…uperset of Interfaces incl. explicit interfaces]]
@Sewer56

Sewer56 commented Jun 27, 2022

Copy link
Copy Markdown
Contributor Author

.NET 5 Annotations

That's a really good point.
I'll see how the linker treats those with a dummy project when I get up later. I just woke up.

I made a dummy project to test this, seems like that's right, so I've updated the annotations to use PublicMethods | NonPublicMethods for .NET 5; which should hopefully be a superset of (COM) interfaces.

Note: The test project uses 3 projects because Microsoft.NET.ILLink.targets (the target that handles assembly trimming during publish) roots the main assembly regardless of whether IsTrimmable is set.

Trimming Dummy

I removed SharpGen.Runtime.COM from the main trimming dummy and made a second dummy project, which is part of the SharpGen.Runtime.COM solution. For cleanliness, I put it in a separate subfolder from main SharpGen.Runtime.COM project and updated build scripts accordingly. Main build script and CI/CD are currently passing.

Original Post

Updated to reflect all changes.

The Remaining Problem

Best just quoted.

So, basically, to everything related to SharpGen and it's users. That's excessive, but I'm not sure if there is a way to preserve semantics. The semantics are: everything that is derived from CppObject can be trimmed safely in their entirety if not referenced (possibly through reflection though). Everything derived from CallbackBase has to always preserve all interface methods that derive from ICallbackable (technically, not true: currently it's all interfaces, since GUID list currently contains even runtime-generated type GUIDs, but that's stupid).

Yeah, I'm not 100% sure if it's possible either without a change in public API. This requires some further investigation.

Edit: I have an idea for a clean way to handle this one actually, will need to test a thing or two after a break.

@Sewer56

Sewer56 commented Jun 27, 2022

Copy link
Copy Markdown
Contributor Author

Okay, think the best approach for the remaining problem (don't preserve all ICallbackable) is to extend the source generator to find all types that inherit from CallbackBase and adding calls to a method like:

static void PreserveInterface<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interface)]T>() { }

I've never worked on a source generator before but this sounds simple enough, I'm gonna give it a go for now.

Unrelated Shenanigans

While investigating possible solutions I ran into a very funny hack that lets you preserve every type that inherits from a type at the expense of 2 x64 instructions. It's too funny not to share.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] // Only 'All' seems to preserve interfaces.
// [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All | DynamicallyAccessedMemberTypes.PublicMethods)] // Doesn't preserve interface contents :(
public class InterfaceImplementer : IGoodBye, IHello
{
    private bool _false = false;
    public InterfaceImplementer()
    {
        if (_false) // Always false
            PreserveMe(GetType()); // GetType exposes class which parents InterfaceImplementer
    }

    public void SayHello() => Console.WriteLine("We're no strangers to love");

    public void SayGoodbye() => Console.WriteLine("Never gonna");

    private static void PreserveMe([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type value) { }
}

public class BaseClassImplementer : InterfaceImplementer, IGoodBye, IHello
{
    public void Execute() => Console.WriteLine("execution. execution. execution. execution.");
}

public interface IGoodBye
{
    void SayGoodbye();
}

public interface IHello
{
    void SayHello();
}

[Unfortunately Linker doesn't have a Preserve All Types that Inherit from Me option as far as I can tell, so injecting in a constructor like this is the closest solution that requires 0 API changes that isn't a source generator, which is what I'll do]

@Sewer56

Sewer56 commented Jun 27, 2022

Copy link
Copy Markdown
Contributor Author

Success!

SharpGen.Runtime
image

[DnSpy Screenshots of Test Assembly]
image
image

[Module Initializer]
image

I managed to make SharpGen prevent types inheriting from CallbackBase from being trimmed away by adding code to the module initializer via the SharpGen Source Generator. This is definitively a much better solution than just blanket preserving ICallbackable; which includes CppObject (which itself doesn't need annotation).

I updated the first post to reflect list of changes as of current commit.

I'm making this post to get some feedback, as I'm pretty tired and am probably done for the day.
[Don't mind the build log, I need to update the Generator to only include types the module initializer can access, I'll fix it and push tomorrow]

@Sewer56

Sewer56 commented Jun 28, 2022

Copy link
Copy Markdown
Contributor Author

I added Analyzer Warnings for any CallbackBase types which cannot be accessed by the module initialiser.

devenv_Em6hpMHV2N

This PR should be closer to a mergeable state now.

Needs more testing for the .NET 5 case, I'll also need to revert the change that replaced All with PublicMethods | PrivateMethods for technical reasons. Updated opening post again to reflect the current state of the PR.

I'm just keeping everyone updated; I'll explicitly mention in the post when the PR is ready for final review and merging.

@Sewer56

Sewer56 commented Jun 28, 2022

Copy link
Copy Markdown
Contributor Author

Success!

SharpGen.Runtime image

[DnSpy Screenshots of Test Assembly]

[Module Initializer]

...

Due to complications and some weird cases where the linker may preserve the interface but link away the internal code, I'm forced to preserve all instead of just the interfaces for now [I might need to open an issue report on linker repo] and have pushed the relevant change to the fork.

Trimming in general is hard to test, however I believe the code is ready for testing on a real project; so I'll try IL Linking Vortice.Windows with my local version of SharpGen and fix any issues as they show along.

@Sewer56

Sewer56 commented Jun 28, 2022

Copy link
Copy Markdown
Contributor Author

I have marked Vortice.Windows for trimming using latest commit d5a2b3b in this PR.

I can at least confirm that the trimming experiment is a success; all Vortice samples:

  • Successfully execute.
  • Give no trimmer warnings.

(CC. @amerkoleci )

Tested Applications

Sizes are for Vortice* + SharpGen* DLLs.

All projects published with:

  • TFM: net6.0-windows
  • TrimmerDefaultAction = link
  • Configuration: Release

You can find my fork of Vortice here if you wish to inspect the results yourself.

Test Results

✔ AdvancedTextRenderingApp

Size: 1,258,496 -> 233,472 bytes

✔ HelloDirect3D11

Size: 1,619,456 -> 327,168 bytes

✔ HelloDirect3D12

Size: 1,232,896 -> 223,232 bytes

✔ HelloDirectML

Size: 1,468,928 -> 138,240 bytes

✔ HelloDirectInput

Size: 763,392 -> 105,472 bytes

✔ AudioPlay

Size: 1,121,792 -> 189,440 bytes

✔ EnumerateDevices

Size: 1,121,792 -> 161,280 bytes

✔ HelloXInput

Size: 12,800 -> 10,752 bytes

[Doesn't actually use SharpGen, just for completeness since I did all the others]

❓ HelloDirect3D12Raytracing

Size: 1,232,896 -> 260,096 bytes

My GPU (R9 290) does not support Raytracing. I cannot verify this sample works.

❓ HelloDirectStorage

Size: 1,175,552 -> 128,512 bytes

Unsupported on my hardware.

@Sewer56 Sewer56 changed the title [WIP] Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime Added: Assembly Trimming Annotations for COM Related Components in SharpGen.Runtime Jun 28, 2022
@amerkoleci

Copy link
Copy Markdown
Contributor

I have marked Vortice.Windows for trimming using latest commit d5a2b3b in this PR.

I can at least confirm that the trimming experiment is a success; all Vortice samples:

  • Successfully execute.
  • Give no trimmer warnings.

(CC. @amerkoleci )

Tested Applications

Sizes are for Vortice* + SharpGen* DLLs.

All projects published with:

  • TFM: net6.0-windows
  • TrimmerDefaultAction = link
  • Configuration: Release

You can find my fork of Vortice here if you wish to inspect the results yourself.

Test Results

✔ AdvancedTextRenderingApp

Size: 1,258,496 -> 233,472 bytes

✔ HelloDirect3D11

Size: 1,619,456 -> 327,168 bytes

✔ HelloDirect3D12

Size: 1,232,896 -> 223,232 bytes

✔ HelloDirectML

Size: 1,468,928 -> 138,240 bytes

✔ HelloDirectInput

Size: 763,392 -> 105,472 bytes

✔ AudioPlay

Size: 1,121,792 -> 189,440 bytes

✔ EnumerateDevices

Size: 1,121,792 -> 161,280 bytes

✔ HelloXInput

Size: 12,800 -> 10,752 bytes

[Doesn't actually use SharpGen, just for completeness since I did all the others]

❓ HelloDirect3D12Raytracing

Size: 1,232,896 -> 260,096 bytes

My GPU (R9 290) does not support Raytracing. I cannot verify this sample works.

❓ HelloDirectStorage

Size: 1,175,552 -> 128,512 bytes

Unsupported on my hardware.

Thanks, currently I'm on vacation, will be back on July 4th and check your changes, thanks again!

@Sewer56

Sewer56 commented Jun 28, 2022

Copy link
Copy Markdown
Contributor Author

@jkoritzinsky @andrew-boyarshin

PR should be ready for review. Opening post should be accurate with current PR changes.
Let me know if you'd like any further changes made.

This was a lot of work for a post-weekend project 😅.
Learned a thing or two along the way, was fun.

@andrew-boyarshin andrew-boyarshin self-requested a review June 28, 2022 16:04
@Sewer56

Sewer56 commented Aug 21, 2022

Copy link
Copy Markdown
Contributor Author

It seems that .NET 7 has a breaking change where all assemblies will be trimmed by default when trimming is enabled.

When I read the announcement, this PR suddenly came back to mind; so I thought I'd spread the news if there was anyone out there who was also interested.

@dmirmilshteyn

Copy link
Copy Markdown

Are there still plans to merge this? I'm trying to use Vortice in an AoT scenario, and the lack of annotations on the currently released version are causing it to completely fail.

@zaafar

zaafar commented Jan 14, 2023

Copy link
Copy Markdown

Are there still plans to merge this? I'm trying to use Vortice in an AoT scenario, and the lack of annotations on the currently released version are causing it to completely fail.

I am having the same issue as this one. Would be nice to get any suggestions/ideas on the workaround.

@jkoritzinsky jkoritzinsky merged commit 29a6d25 into SharpGenTools:main Jan 14, 2023
@Sewer56

Sewer56 commented Jan 15, 2023

Copy link
Copy Markdown
Contributor Author

I should be able to go back to my Vortice branch and submit the PR to add trimmer annotations now.

Just need a package to make it nuget.org, I don't think @amerkoleci would be comfortable with having a dependency a nightly package from a MyGet feed. (At least I wouldn't). It'd cause a few issues 😅.

@zaafar

zaafar commented Jan 16, 2023

Copy link
Copy Markdown

Whenever you folks get some time, please create a new (beta) package :)

@amerkoleci

Copy link
Copy Markdown
Contributor

Tag: v2.0.0-beta.12 has been released and new nuget is on the way.

@amerkoleci

Copy link
Copy Markdown
Contributor

Not sure why SharpGen.Runtime.COM didn't being updated to latest tag, any thought @andrew-boyarshin @jkoritzinsky ?

@Sewer56

Sewer56 commented Jun 18, 2023

Copy link
Copy Markdown
Contributor Author

It just dawned on me I missed the whole NuGet package release (oops!).
Well, time to resurrect the Vortice trimming branch.

I'm about to finish my first ever Rust library (port of one of my C# libraries); I'll get on it right after that.

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

Successfully merging this pull request may close these issues.

6 participants