puerts icon indicating copy to clipboard operation
puerts copied to clipboard

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

Open mysticfarer opened this issue 1 year ago • 8 comments

前置阅读 | 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
    • 关键代码: https://github.com/Tencent/puerts/blob/a565f517899da38c71dc4f2f8e480b86abc12cb7/unreal/Puerts/Source/JsEnv/Private/PropertyTranslator.cpp#L903
  2. 在MulticastFooArray执行期间, this.FooArray出现了扩容, 其旧FScriptArray.Data失效
  3. 在MulticastFooArray执行结束后, 拷贝出参时, 写入了上一步已失效的FScriptArray.Data, 造成内存写越界, 进而在后续环节崩溃
    • 关键代码: https://github.com/Tencent/puerts/blob/a565f517899da38c71dc4f2f8e480b86abc12cb7/unreal/Puerts/Source/JsEnv/Private/FunctionTranslator.cpp#L614

日志:

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 avatar Jan 07 '25 07:01 mysticfarer

首先得避免直接为了满足传参要求而直接 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调用成员方法默认是不经过蓝图的,感觉即使按你这写法应该也不至于崩,你看看你这里是怎样的:https://github.com/Tencent/puerts/commit/c961363e4c71f875383705a544a649a80b03b47d

chexiongsheng avatar Jan 08 '25 05:01 chexiongsheng

感谢车神回复:D

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

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

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

mysticfarer avatar Jan 08 '25 06:01 mysticfarer

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

chexiongsheng avatar Jan 08 '25 08:01 chexiongsheng

https://github.com/Tencent/puerts/blob/a565f517899da38c71dc4f2f8e480b86abc12cb7/unreal/Puerts/Source/JsEnv/Private/FunctionTranslator.cpp#L265

这的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 avatar Jan 08 '25 08:01 chexiongsheng

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

chexiongsheng avatar Jan 08 '25 08:01 chexiongsheng

https://github.com/Tencent/puerts/blob/a565f517899da38c71dc4f2f8e480b86abc12cb7/unreal/Puerts/Source/JsEnv/Private/FunctionTranslator.cpp#L265

这的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 avatar Jan 08 '25 11:01 chexiongsheng

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

chexiongsheng avatar Jan 08 '25 12:01 chexiongsheng

用一个比较别扭但是改动比较小的方案绕过去了。

  1. 所有FArrayScript的JS包装额外增加一个InternalField作为标记
  2. SlowCall的时候如果发现有引用传参的FScriptArray,把InternalField的标记设置为true
  3. 所有FScriptArray Wrapper会导致重分配内存的函数(add/remove/empty)都要检查这个标记,不允许扩容或者缩容
  4. SlowCall结束后恢复标记

BlurryLight avatar Feb 07 '25 14:02 BlurryLight