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

[UE] Bug: $InRef<UE.TArray>与数组扩容引起的一种崩溃情况 #1963

Open
3 tasks done
mysticfarer opened this issue Jan 7, 2025 · 7 comments
Open
3 tasks done
Assignees
Labels
bug Something isn't working Unreal

Comments

@mysticfarer
Copy link
Contributor

前置阅读 | Pre-reading

Puer的版本 | Puer Version

latest master

UE的版本 | UE Version

5.3 / 5.5

发生在哪个平台 | Platform

Editor(win)

错误信息 | Error Message

此崩溃与函数出参拷贝, 及数组扩容相关,
详见下述 "问题重现".

问题成因虽然比较清楚, 但对如何防止这种情况, 或及时检查到此类问题, 比较困扰,
还请车神指教, 感谢:D

问题重现 | Bug reproduce

[1/5] 打开 puerts_unreal_demo 工程

[2/5] 打开 "继承引擎类功能"

[3/5] 新建 MyPlayerController.ts 文件, 并粘贴下述代码

MyPlayerController.ts

import { $InRef, $Ref, $ref, $unref } from 'puerts';
import * as UE from 'ue';
import { rpc } from 'ue';

class MyPlayerController extends UE.PlayerController
{
    ReceiveBeginPlay(): void
    {
        this.FooArray.Add(11);
        this.FooArray.Add(22);

        this.PrintTArray('MulticastFooArray - Pre: this.FooArray', this.FooArray);
        this.MulticastFooArray($ref(this.FooArray));
        this.PrintTArray('MulticastFooArray - Post: this.FooArray', this.FooArray);
    }

    @rpc.flags(rpc.FunctionFlags.FUNC_Net | rpc.FunctionFlags.FUNC_NetMulticast | rpc.FunctionFlags.FUNC_NetReliable)
    private MulticastFooArray(InArrayRef: $InRef<UE.TArray<number/*@cpp:int*/>>): void
    {
        let InArray = $unref(InArrayRef);
        this.PrintTArray('MulticastFooArray - StepIn: InArray', InArray);
        this.PrintTArray('MulticastFooArray - StepIn: this.FooArray', this.FooArray);

        for (let i = 0; i < 8; ++i)
        {
            this.FooArray.Add(111 + i);
        }
        this.PrintTArray('MulticastFooArray - this.FooArray changed: InArray', InArray);
        this.PrintTArray('MulticastFooArray - this.FooArray changed: this.FooArray', this.FooArray);
    }

    // @no-blueprint
    private PrintTArray(InPrefix: string, InArray: UE.TArray<number>): void
    {
        for (let i = 0; i < InArray.Num(); ++i)
        {
            let n = InArray.Get(i);
            console.log(`${InPrefix}: i=${i}, value=${n}`);
        }
    }

    private FooArray: UE.TArray<number/*@cpp:int*/>;
}

export default MyPlayerController;

[4/5] 修改GameMode, 应用MyPlayerController

[5/5] 在Editor中Play, 即可重现崩溃

无需选择网络模式, 用默认的Standalone模式即可.

线索:

  1. 在调用 this.MulticastFooArray($ref(this.FooArray)); 时, 有一次对FScriptArray的浅拷贝, 记录下了this.FooArrayFScriptArray.Data
  2. 在MulticastFooArray执行期间, this.FooArray出现了扩容, 其旧FScriptArray.Data失效
  3. 在MulticastFooArray执行结束后, 拷贝出参时, 写入了上一步已失效的FScriptArray.Data, 造成内存写越界, 进而在后续环节崩溃

日志:

Puerts: (0x00000559E381C8D0) MulticastFooArray - Pre: this.FooArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - Pre: this.FooArray: i=1, value=22

Puerts: (0x00000559E381C8D0) MulticastFooArray - StepIn: InArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - StepIn: InArray: i=1, value=22
Puerts: (0x00000559E381C8D0) MulticastFooArray - StepIn: this.FooArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - StepIn: this.FooArray: i=1, value=22

Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: InArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: InArray: i=1, value=22

Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=1, value=22
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=2, value=111
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=3, value=112
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=4, value=113
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=5, value=114
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=6, value=115
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=7, value=116
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=8, value=117
Puerts: (0x00000559E381C8D0) MulticastFooArray - this.FooArray changed: this.FooArray: i=9, value=118

Puerts: (0x00000559E381C8D0) MulticastFooArray - Post: this.FooArray: i=0, value=11
Puerts: (0x00000559E381C8D0) MulticastFooArray - Post: this.FooArray: i=1, value=22
@mysticfarer mysticfarer added bug Something isn't working Unreal labels Jan 7, 2025
@chexiongsheng
Copy link
Collaborator

首先得避免直接为了满足传参要求而直接 this.MulticastFooArray($ref(this.FooArray));
引用类型相当于一个输入和一个输出,输出其实是新值,旧值是脏值,直接$ref临时值传参即使是number语义上也不对。
合理的做法应该是这样:

this.PrintTArray('MulticastFooArray - Pre: this.FooArray', this.FooArray);
const r = $ref(this.FooArray);
this.MulticastFooArray(r);
this.FooArray = $unref(r); // 新值覆盖旧值
this.PrintTArray('MulticastFooArray - Post: this.FooArray', this.FooArray);

你试试还会崩不。
当然,作为底层不恰当的用法导致崩溃也应该解决。

另外,新版本js调用成员方法默认是不经过蓝图的,感觉即使按你这写法应该也不至于崩,你看看你这里是怎样的:c961363

@mysticfarer
Copy link
Contributor Author

感谢车神回复:D

  1. 确认我的环境中, bForceAllUFunctionInCPP = false

    • 注: 在重现示例里用了 @rpc.flags(rpc.FunctionFlags.FUNC_Net, ...) ,有FUNC_Net标志的函数是会经过虚幻蓝图ProcessEvent的,不会直接在js中执行。
  2. 明白且赞同车神的合理做法,代码确实应该这样写。

    • 但比较棘手的是,在业务复杂、协作同事多的时候,难免在 MulticastFooArray 这类函数中会出现修改输入参数的行为,一旦输入参数是数组,且此数组在函数内扩容,就会发生写越界,并在之后的随机位置崩溃。

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jan 8, 2025

我跟踪了下,感觉Puerts的处理是符合预期的(浅拷贝过去,处理完成后浅拷贝回来)。问题也不在调用方怎么做。
而是你MulticastFooArray好像挺不合理的。你放着传入的InArrayRef参数不用,而是通过this访问了原数组,导致其扩容,然后浅拷贝过去的参数没做任何的操作,还是持有的旧数据(坏数据),然后回来时浅拷贝到数组里头导致的崩溃。

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jan 8, 2025

auto CallFunctionPtr = CallFunction.Get();

这的SlowCall分支会有这问题
注释掉SlowCall,只保留FastCall分支是没问题的

    auto CallFunctionPtr = CallFunction.Get();
    //if ((Function->FunctionFlags & FUNC_Native) && !(Function->FunctionFlags & FUNC_Net) &&
    //    !CallFunctionPtr->HasAnyFunctionFlags(FUNC_UbergraphFunction))
    //{
        FastCall(Isolate, Context, Info, CallObject, CallFunctionPtr, Params);
    //}
    //else
    //{
    //    SlowCall(Isolate, Context, Info, CallObject, CallFunctionPtr, Params);
    //}

我建议你在你们项目试试,如果整个项目跑一段时间没发现问题我直接在主线去掉好了。

@chexiongsheng
Copy link
Collaborator

这问题要解决其实就是按FastCall来。

@chexiongsheng
Copy link
Collaborator

auto CallFunctionPtr = CallFunction.Get();

这的SlowCall分支会有这问题 注释掉SlowCall,只保留FastCall分支是没问题的

    auto CallFunctionPtr = CallFunction.Get();
    //if ((Function->FunctionFlags & FUNC_Native) && !(Function->FunctionFlags & FUNC_Net) &&
    //    !CallFunctionPtr->HasAnyFunctionFlags(FUNC_UbergraphFunction))
    //{
        FastCall(Isolate, Context, Info, CallObject, CallFunctionPtr, Params);
    //}
    //else
    //{
    //    SlowCall(Isolate, Context, Info, CallObject, CallFunctionPtr, Params);
    //}

我建议你在你们项目试试,如果整个项目跑一段时间没发现问题我直接在主线去掉好了。

这应该不行,FastCall应该会导致RPC发不了。

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jan 8, 2025

FastCall不会有这样的问题,但FastCall不支持RPC。
SlowCall调用ProcessEvent,能支持RPC。但接口决定了会有这样的问题:传不了FFrame参数,所以没法对Out参数做特别的处理。
FastCall其实逻辑是参考ProcessEvent的,只不过移除了不必要的部分(包括RPC)。
所以思路应该是参考ProcessEvent在FastCall里加入RPC的支持,然后统一到FastCall,不用SlowCall。
你们可以按这思路处理下,成功了PR给我。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Unreal
Projects
None yet
Development

No branches or pull requests

2 participants