[UE] Bug: $InRef<UE.TArray>与数组扩容引起的一种崩溃情况
前置阅读 | 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模式即可.
线索:
- 在调用 this.MulticastFooArray($ref(this.FooArray)); 时, 有一次对FScriptArray的浅拷贝, 记录下了
this.FooArray的FScriptArray.Data- 关键代码: https://github.com/Tencent/puerts/blob/a565f517899da38c71dc4f2f8e480b86abc12cb7/unreal/Puerts/Source/JsEnv/Private/PropertyTranslator.cpp#L903
- 在MulticastFooArray执行期间,
this.FooArray出现了扩容, 其旧FScriptArray.Data失效 - 在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
首先得避免直接为了满足传参要求而直接 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
感谢车神回复:D
-
确认我的环境中,
bForceAllUFunctionInCPP = false- 注: 在重现示例里用了
@rpc.flags(rpc.FunctionFlags.FUNC_Net, ...),有FUNC_Net标志的函数是会经过虚幻蓝图ProcessEvent的,不会直接在js中执行。
- 注: 在重现示例里用了
-
明白且赞同车神的合理做法,代码确实应该这样写。
- 但比较棘手的是,在业务复杂、协作同事多的时候,难免在
MulticastFooArray这类函数中会出现修改输入参数的行为,一旦输入参数是数组,且此数组在函数内扩容,就会发生写越界,并在之后的随机位置崩溃。
- 但比较棘手的是,在业务复杂、协作同事多的时候,难免在
我跟踪了下,感觉Puerts的处理是符合预期的(浅拷贝过去,处理完成后浅拷贝回来)。问题也不在调用方怎么做。 而是你MulticastFooArray好像挺不合理的。你放着传入的InArrayRef参数不用,而是通过this访问了原数组,导致其扩容,然后浅拷贝过去的参数没做任何的操作,还是持有的旧数据(坏数据),然后回来时浅拷贝到数组里头导致的崩溃。
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来。
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发不了。
FastCall不会有这样的问题,但FastCall不支持RPC。 SlowCall调用ProcessEvent,能支持RPC。但接口决定了会有这样的问题:传不了FFrame参数,所以没法对Out参数做特别的处理。 FastCall其实逻辑是参考ProcessEvent的,只不过移除了不必要的部分(包括RPC)。 所以思路应该是参考ProcessEvent在FastCall里加入RPC的支持,然后统一到FastCall,不用SlowCall。 你们可以按这思路处理下,成功了PR给我。
用一个比较别扭但是改动比较小的方案绕过去了。
- 所有FArrayScript的JS包装额外增加一个InternalField作为标记
- SlowCall的时候如果发现有引用传参的FScriptArray,把InternalField的标记设置为true
- 所有FScriptArray Wrapper会导致重分配内存的函数(add/remove/empty)都要检查这个标记,不允许扩容或者缩容
- SlowCall结束后恢复标记