InjectFix icon indicating copy to clipboard operation
InjectFix copied to clipboard

Unity2017下async函数修复后执行异常

Open xuyanghuang-tencent opened this issue 3 years ago • 2 comments

版本:Unity2017.4.40c1 image

重现代码:


using IFix.Core;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        var patchPath = "./Assembly-CSharp.patch.bytes";
        if (File.Exists(patchPath))
        {
            PatchManager.Load(new FileStream(patchPath, FileMode.Open));
            Debug.Log("patch file loaded:" + patchPath);
        }

        CallOneAsync("hello");
    }

    [IFix.Patch]
    public async void CallOneAsync(string name)
    {
        Debug.Log(name);
        await Task.Delay(100);
        Debug.Log(name);
        await Task.Delay(100);
        Debug.Log(name);
    }
}

生成Patch并Inject后启动游戏: image

期望结果: 日志输出三次hello

同样的代码在Unity2019.4.19f1是能够正常工作的。

xuyanghuang-tencent avatar May 15 '22 05:05 xuyanghuang-tencent

触发这个问题的原因:

Unity2017和Unity2019为运行时环境生成的IAsyncStateMachine状态机前者是struct后者是class image

CodeTranslator里为类似<CallOneAsync>c__async0桥接类型时,在addAnonymousCtor中,误把第一个方法作为匿名类型的构造方法。在为class的情况下是正常的.ctor,但在struct的情况下拿到的则是MoveNext

VirtualMachine运行时执行补丁代码时,会在Code.Newanon中构造出上面的桥接类型,并调用构造方法,但实际上调用的是MoveNext。所以看起来似乎是执行了async流程,调用到了CallOneAsync中第一行的Debug.Log(name)。而这时还没有执行真正的CallOneAsync,也就是<CallOneAsync>c__asyncname这些属性都还没有赋值,所以日志输出的name就是Null了,同理整个状态机也没有启动,所以也不会执行后续阶段的异步代码。

xuyanghuang-tencent avatar May 15 '22 05:05 xuyanghuang-tencent

处理构造方法比较简单,在生成匿名类型信息时,如果默认方法不是构造方法直接写-1,运行时执行Code.Newanon时跳过,这样就能正常执行CallOneAsync并启动async状态机,从而在后续正确的流程中调用MoveNext

但还有个问题是MoveNext执行完第一阶段时,会进入到: this.$builder.AwaitUnsafeOnCompleted<TaskAwaiter, Test.<CallOneAsync>c__async0>(ref this.$awaiter0, ref this);

然后进入到m_coreState.PostBoxInitializationimage image

这里的stateMachine已经被包装成了注入后的桥接对象ILFixInterfaceBridge,也就是说m_coreState.m_stateMachine在第一行就已经被赋值了。

第二行会进入被补丁的SetStateMachine,从而执行到this.$builder.SetStateMachine(stateMachine);,这个$builder是个AsyncVoidMethodBuilder,里边的逻辑是: image

这里的m_coreStateAsyncMethodBuilderCore,由于前面已经赋值过m_stateMachine,最终导致抛出异常: image

而struct因为需要拷贝所以不会导致这个问题,另外Unity2019环境下虽然也是class,但编译器为async状态机对象生成出来的SetStateMachine是个空实现,所以也不会有这个问题。

目前想到的解决方法是在生成桥接对象时,如果是编译器生成的struct且方法为SetStateMachine,就直接留空实现。

xuyanghuang-tencent avatar May 15 '22 06:05 xuyanghuang-tencent