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

Add awaiter implementation #133

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

extraes
Copy link

@extraes extraes commented Jun 17, 2024

Adds Pass61ImplementAwaiters
Looks for types implementing INotifyCompletion and generates new methods that allow the interop types to implement that interface, calling the Il2CppSystem.Action -> System.Action implicit conversion before calling the original method.

This makes originally-awaitable types (e.g. UniTasks, if the game has them) awaitable again.

I previously did this in a much less automated way with Cecil in another project, but the runtime didn't like what I was doing, so I figured I'd make it less janky and add it to Il2CppInterop, and sure enough the runtime no longer rejects the type.

@ds5678 ds5678 added generation Related to assembly generation enhancement New feature or request labels Jul 15, 2024
@ds5678

This comment was marked as outdated.

@ds5678
Copy link
Collaborator

ds5678 commented Dec 22, 2024

I investigated this pull request and what it's attempting to solve. I discovered that the type system improvements I mentioned above will not automatically fix the issue.

As I understand it, the issue is that even if proper interface support is implemented with system interface support, only the Il2Cpp INotifyCompletion interface will be implemented since the generated OnCompleted method uses Il2CppSystem.Action instead of System.Action. Your pull request generates a helper method to avoid this.

For the rewrite, I could see a generic implementation where all Il2Cpp Action and Func delegates in method signatures are converted to their System counterparts, but I can also see your narrow implementation persisting into the rewrite if the generic implementation proves difficult or has some undesirable outcomes.

I am willing to review this pull request once it has been ported to AsmResolver.

Reference: https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/INotifyCompletion.cs

Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
public static void DoPass(RewriteGlobalContext context)
{
var corlib = context.GetAssemblyByName("mscorlib");
var actionUntyped = corlib.GetTypeByName("System.Action");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we seriously not use separate namespace and name when doing this throughout the whole project? That seems so inefficient.

Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs Outdated Show resolved Hide resolved
- Use CorLibTypeFactory.Void for void reference
- Added a couple more early-outs before LINQ
- Use .Single
- Use MemberCloner (and define a nested type for that)
- Check method signature & forward to methods that may have been wonkily unhollowed
@ds5678
Copy link
Collaborator

ds5678 commented Dec 25, 2024

Whenever you're ready for another review, let me know.

@extraes
Copy link
Author

extraes commented Dec 26, 2024

Ah yeah, if you can review again that'd be great

Copy link
Collaborator

@ds5678 ds5678 left a comment

Choose a reason for hiding this comment

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

I gave it another pass. 👍

var corlib = context.CorLib;
var actionUntyped = corlib.GetTypeByName("System.Action");

var actionConversion = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Methods.Single

foreach (var typeContext in assemblyContext.Types)
{
// Used later for MemberCloner, just putting up here as an early exit in case .Module is ever null
if (typeContext.NewType.Module is null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be impossible. I don't mind the null check, but I think a Debug.Assert would be better.

Copy link
Author

Choose a reason for hiding this comment

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

This check prevents the compiler from warning later down the line. I can replace it with a Debug.Assert if you'd like, but I'd then have to either ignore the warnings or ! the nullability away.

if (typeContext.OriginalType.IsInterface || typeContext.OriginalType.Interfaces.Count == 0)
continue;

var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(interfaceImpl => interfaceImpl.Interface?.Name == nameof(INotifyCompletion));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would prefer you check the namespace too.

if (allOnCompleted.Length == 0)
{
// Likely defined as INotifyCompletion.OnCompleted & the name is unhollowed as something like "System_Runtime_CompilerServices_INotifyCompletion_OnCompleted"
allOnCompleted = typeContext.NewType.Methods.Where(m => ((string?)m.Name)?.EndsWith(nameof(INotifyCompletion.OnCompleted)) ?? false).ToArray();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can check the method contexts for the exact name.


// Conversion spits out an Il2CppSystem.Action, so look for methods that take that (and only that) in & return void, so the stack is balanced
// And use IsAssignableTo because otherwise equality checks would fail due to the TypeSignatures being different references
var interopOnCompleted = allOnCompleted.FirstOrDefault(m => m.Parameters.Count == 1 && m.Signature is not null && m.Signature.ReturnType == voidRef && SignatureComparer.Default.Equals(m.Signature.ParameterTypes[0], actionConversion.Signature?.ReturnType));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use signature comparer instead of equality for the void check.

You also need to check that it's an instance method.

typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value));

var instructions = body.Instructions;
instructions.Add(CilOpCodes.Nop);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unnecessary.

Copy link
Author

Choose a reason for hiding this comment

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

The nops are there because I was copying the IL I got from a normally-compiled OnComplete. I'll remove them.

instructions.Add(CilOpCodes.Call, interopOnCompleted);
}

instructions.Add(CilOpCodes.Nop);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unnecessary.

}

// Conversion spits out an Il2CppSystem.Action, so look for methods that take that (and only that) in & return void, so the stack is balanced
// And use IsAssignableTo because otherwise equality checks would fail due to the TypeSignatures being different references
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like you no longer use IsAssignableTo.

Copy link
Author

Choose a reason for hiding this comment

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

Ah yeah that was a comment from before I saw SignatureComparer. I'll change it to reflect the current state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request generation Related to assembly generation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants