unreal3脚本stacktrace的问题

时间:2023-03-09 04:33:35
unreal3脚本stacktrace的问题

在unrealscript里获取调用栈,有下面两函数:

/**
* Dumps the current script function stack to the log file, useful
* for debugging.
*/
native static final function ScriptTrace(); /**
* Gets the current script function stack back so you can log it to a specific log location (e.g. AILog).
*/
native static final function String GetScriptTrace();

这两个是Object上的静态函数,也就相当于全局函数了

最终都会调用到c++里的函数:

/**
* This will return the StackTrace of the current callstack from .uc land
**/
inline FString FFrame::GetStackTrace() const
{
FString Retval; // travel down the stack recording the frames
TArray<const FFrame*> FrameStack;
const FFrame* CurrFrame = this;
while( CurrFrame != NULL )
{
FrameStack.AddItem(CurrFrame);
CurrFrame = CurrFrame->PreviousFrame;
} // and then dump them to a string
Retval += FString( TEXT("Script call stack:\n") );
for( INT idx = FrameStack.Num() - ; idx >= ; idx-- )
{
const FFrame& f = *FrameStack(idx);
Retval += FString::Printf( TEXT("\t%s %d\n"), *f.Node->GetFullName(), f.Node->Line );
} return Retval;
}

这个f.Node->Line也就是行号,原来并没有,是我加上去的。但其实也没有太大作用。

因为对于调用栈回溯,最重要的是知道每一层函数当前执行到哪一行了,而这里的Line只是函数定义块首句所在那一行。

如果A调用了B,且在A的函数体内,有多个地方调用B,那么对于一个没有实际执行行号的stacktrace,就不知道当前A是执行到哪一句才进入B的。

要实现上述功能,只记录一个函数定义首行肯定是不够的,必须建立字节码和源码行数的映射表,这个映射表可能很大,在编译release模式时可以通过开关去除。(比如lua就是这样做的)

为什么unrealscript里没做类似的事呢,难道他们内部人员开发中不需要用到?

我觉得这可能与unrealscript的设计理念有关,因为根据文档所说,unrealscript是没有异常机制的:

  • UnrealScript doesn't support generics, annotations or exception handling. Things like NullPointerException or IndexOutOfBoundsException are handled gracefully in UnrealScript by just returning null values after writing a warning message to the log file. Typecasting object references is safe and also works for empty references.

以及:

The idea behind using return types in functions (or methods if you will) is to make sure it cannot return any other thing, and if it does, cast an Exception...
You can't return anything from a function with no return type, nor can you return anything of the wrong type (except in cases where there is an automatic cast) - the compiler will give an error. That's much better than throwing an exception.

以及:

having an exception handler would also add that the game halts on non-trapped exceptions. You really wouldn't want your game halting and dying on any error. Besides, there's only a handful of possible error conditions that can happen at runtime, anyway.

总结来说,就是unrealscript本身是一种强类型的编译型脚本,很多问题在编译期就可以发现了,而一旦进入运行期,遇到错误,首发目标是对它进行兼容处理,而非抛出异常。比如很典型的【Access None】,这在c++里就是访问空指针这样严重错误了,但unrealscript并不抛出异常,只是简单的打一个warn到log,然后默默的执行下一句代码。。

这样的好处是,函数的大部份语句得以执行,不会因某一句的错误,而中断、改变了流程。

既然是这样,那stack trace确实没多大的用处了,因为它的作用就是出现异常的时候,去了解出错时的调用链。

几个附加备忘:

1、关于函数定义的行号,实际上并没有跟源文件中函数声明那一行严格匹配,这让我曾一度很困惑,但是通过观察调试脚本编译流程,知道了原因所在:函数的行号,实际是它体内第一句“非声明型语句”所在的那一行。也就是头部所有【local classXXX varYYY】这样的语句都不算数,直到碰到【x=y】这样的才算第一句代码。具体的实现在UnSrcCom.cpp里:

UBOOL FScriptCompiler::CompileStatement()
{
UBOOL NeedSemicolon = ; // Get a token and compile it.
FToken Token;
if( !GetToken(Token,NULL,) )
{
// End of file.
return ;
}
else if( !CompileDeclaration( Token, NeedSemicolon ) )
{
if( Pass == PASS_Parse )
{
// Skip this and subsequent commands so we can hit them on next pass.
if( NestLevel < )
{
ScriptErrorf(SCEL_Unknown/*SCEL_Formatting*/, TEXT("Unexpected '%s'"), Token.Identifier );
}
UngetToken(Token);
PopNest( TopNest->NestType, NestTypeName(TopNest->NestType) );
SkipStatements( , NestTypeName(TopNest->NestType) );
NeedSemicolon = ; ……

在CompileDeclaration失败后(也就是不再是一条声明语句时),才转入PopNest,也就是终结上一个AST节点的处理,也是在那里才对Function节点的Line属性赋值:

void FScriptCompiler::PopNest( ENestType NestType, const TCHAR* Descr )
{
……// Pass-specific handling.
if( Pass == PASS_Parse )
{
// Remember code position.
if( NestType==NEST_State || NestType==NEST_Function )
{
TopNode->TextPos = InputPos;
TopNode->Line = InputLine;
……

而走到这一步之前,已经查看了好多Token,也就是往前读过了好多字符,包括换行,所以这里的InputLine已经不是函数声明所在那一行,而是发现当前AST是非声明语句的那一行了。

以上是在编译【完全的函数定义】时的情形,对于一句话式的声明,如:

native static final function ScriptTrace();

这种又当如何呢?在这种情形下,结尾的那个分号,就可以归约一句函数声明,从而在CompileDeclaration->CompileFunctionDeclaration里也调用到PopNest,进而对InputLine赋值,这时InputLine就是当前行,所以这种函数的行号是准确匹配的。

2、在vc里调试时,为了方便观察特定情况,需要设条件断点,通常可以对名字类的字符串进行比较来达到目的。而Unreal3里表示字符串的FString和FName都是很trick的结构,无法直接拿它们跟literals比较(主要是断点条件表达式并不支持c++重载这种高级语法),需要拿到这两个结构里真正的数据地址,才能进行比较:

FString:wcscmp(L"Engine.GameViewportClient",Value.AllocatorInstance.Data)==0,注意Unreal3用的是Unicode字符串

FName:strcmp("GameViewportClient",((FNameEntry**)InClass->Name.Names.AllocatorInstance.Data)[InClass->Name.Index]->AnsiName)==0