-
Notifications
You must be signed in to change notification settings - Fork 151
VScript leak fixes #331
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
VScript leak fixes #331
Conversation
3b8aaa1
to
8fb72ad
Compare
2be9821
to
316b5d3
Compare
316b5d3
to
3d5e1f7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand the purpose of HSCRIPT_RC
for objects that are created by C++, passed to VScript and there may create objects that keep a reference to that "base instance", which will have it's reference-count increased. This allows safe usage of KeyValues and sub-keys thereof. The premise makes sense and I checked the related changes, albeit I didn't make up a complete metal model of how it is used. It looked pretty sensible insofar.
It is also used for CScriptGameTrace
, CTakeDamageInfo
and FireBulletsInfo_t
, which (despite the name implicating this) are not reference-counted, but similarly created by C++ and passed to VScript, from where the lifetime must be managed. This relied on script-authors manually calling provided "Destroy"-functions, which are retained but now no-ops to maintain compatibility. Destruct
-functions are now registered using DEFINE_SCRIPT_REFCOUNTED_INSTANCE()
- except for FireBulletsInfo_t
, which already got its' one through DEFINE_SCRIPT_CONSTRUCTOR()
.
I don't quite know about the necessity of CopyObject()
for IScriptVGUIObject::SetScriptInstance()
/SetHScript()
. I guess it shall ensure the HSCRIPT
stays valid while it is stored there, regardless of what happens with the original reference. Whatever the concrete situation may be that requires this change, the change seems sensible to me as well.
@@ -739,6 +769,10 @@ static inline int ToConstantVariant(int value) | |||
#define DEFINE_SCRIPT_INSTANCE_HELPER( p ) pDesc->pHelper = (p); | |||
|
|||
#ifdef MAPBASE_VSCRIPT | |||
// Allow instance to be deleted but not constructed | |||
// Not needed if the class has a constructor | |||
#define DEFINE_SCRIPT_REFCOUNTED_INSTANCE() do { pDesc->m_pfnDestruct = &CScriptConstructor<_className>::Destruct; } while (0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably name this ScriptAddDestructorToClassDesc()
(mirroring ScriptAddConstructorToClassDesc()
). I don't think REFCOUNTED_INSTANCE
quite represents the meaning of this, even if you use this for a ref-counted data-type right now.
Having no construction, only destruction, is not really specific to ref-counted objects. Anything that vscript cannot create itself, but manages some state that needs to be cleaned up via a destruction-function would use this.
It would also be perfectly reasonable to have a ref-counted object that can be created from vscript.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEFINE_SCRIPT_REFCOUNTED_INSTANCE
is mirroring DEFINE_SCRIPT_CONSTRUCTOR
. The name REFCOUNTED_INSTANCE
is about intent instead of internals. Someone looking to register a C++ class doesn't need to know how it works. This could also be added to classes with constructors and the duplicated assignment would likely be optimised out.
Having no construction, only destruction, is not really specific to ref-counted objects. Anything that vscript cannot create itself, but manages some state that needs to be cleaned up via a destruction-function would use this.
What are the examples of this?
It would also be perfectly reasonable to have a ref-counted object that can be created from vscript.
This is what constructors do, that is the case in FireBulletsInfo_t
. It can be both constructed in script and returned from CreateFireBulletsInfo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name
REFCOUNTED_INSTANCE
is about intent instead of internals. Someone looking to register a C++ class doesn't need to know how it works.
I may have missed something here. I was under the impression that this refers to reference-counting in C++, like the CScriptKeyValues
do, though it may be intertwined with the ref-counting done through the SquirrelVM? Doesn't every script-instance do reference-counting though, (the reference-counting portion of) which is completely detached to destructors/Destruct-functions?
What are the examples of this?
Well, this is a new implementation. I don't really have any new things in mind for which I'd like to use this.
There are existing classes that you're converting though that don't have intrinsic ref-counting, like the FireBulletsInfo_t
you mention. Or does that happen somewhere behind the scenes, or did you intend to refer to the SquirrelVM reference-counting as I wonder about above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All non-primitive (not int, bool, float) script objects always do reference counting in Squirrel. DEFINE_SCRIPT_REFCOUNTED_INSTANCE
and HSCRIPT_RC
are for taking advantage of that. CScriptKeyValues
is a kind of its own because it can create children that have memory that depend on the root, so it manually does reference counting of KeyValues
in C++ independent of Squirrel, which only reference count for CScriptKeyValues
.
Destructors need to be defined for script refcounted objects so that C++ can allocate memory, pass it to RegisterInstance
then forget it all. Otherwise both the script object (HSCRIPT
) and the memory it points to (T *pInstance
) needs to be freed in C++ (see CBaseEntity
).
I tried to explain this and how script objects work in the long reply below.
// if bRefCounted is true, pInstance memory will be deleted by the script, | ||
// returning the result will then behave as if the instance was constructed in script. | ||
// Functions that return the result of this need to return HSCRIPT_RC | ||
virtual HSCRIPT RegisterInstance( ScriptClassDesc_t *pDesc, void *pInstance, bool bRefCounted = false ) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bRefCounted
seems to have a fixed value at the call-sites, which makes sense I think. If you return a HSCRIPT
you can probably determine statically if it's meant to be handled as "ref-counted".
Consider if it is feasible to split the function into two - HSCRIPT RegisterInstance()
and HSCRIPT_RC RegisterInstanceRefcounted()
. Casts from and to HSCRIPT
for HSCRIPT_RC
should then also be made explicit
.
Same with IScriptManager::CreateScriptKeyValues()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding RegisterInstanceRefcounted
would mean also duplicating the SquirrelVM::RegiserInstance
function. A template or a wrapper could work for simplification.
I like the non-explicit nature of HSCRIPT_RC
. It's still a valid HSCRIPT
object; it can be called, cached, deleted. It doesn't require any change other than return type. I also thought of specifying the refcounted nature in function registration, but that was too noisy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it stands, the programmer must take care to convert the return-value to HSCRIPT_RC
- as the comment mentions. This could be enforced by the type-system instead.
Anyway, this is just a drive-by review. I didn't spot a problematic case in the changes FWIW. Feel free to mark as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand what you mean. I added HSCRIPT_RC RegisterInstanceRefCounted()
, removed bRefCounted
parameter and HSCRIPT_RC::operator HSCRIPT()
, added ScriptVariant_t::operator=( HSCRIPT_RC )
. And it works to enforce return type, but it breaks passing HSCRIPT_RC
to HSCRIPT
parameters to call, copy or delete. I'll just leave it as is without a clean solution for now.
} | ||
|
||
// destructor | ||
CScriptKeyValues::~CScriptKeyValues( ) | ||
{ | ||
if (m_pKeyValues) | ||
// Children are always borrowed | ||
Assert( !m_pBase || m_pSelf->borrow ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine the reverse also be true (if it's borrowed it's a child) - Assert( !m_pSelf->borrow || m_pBase )
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not the case for registering C++ managed KeyValues, such as in CLogicExternalData::ScriptGetKeyValues()
. Though in this particular case the entity can die while CScriptKeyValues
lives. Registering a copy would make editing the result of GetKeyValues()
not change m_pRoot
, but require SetKeyValues()
to update. A cumbersome workaround would be CLogicExternalData
keeping copies of script instances it registers. I think @Blixibon could give his input on this since it was his design choice.
Logically it makes sense, and it works given the borrowed memory isn't independently freed before the script VM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember my exact rationale for having CScriptKeyValues
directly wrap the KeyValues
data in logic_externaldata
, although that I may have thought it would be more efficient or intuitive to directly access it. In retrospect, I think that was a poor choice. It may not have occurred to me that the associated data can be removed while the CScriptKeyValues
instance remains.
I like the idea of keeping copies of the script instances to perform proper cleanup while maintaining the current behavior, even if it is cumbersome, although that's not particularly relevant to the rest of this PR and can be resolved another time.
When instances are registered in When the result of For test.txt:
This is used in script function callbacks cached in C++ everywhere:
|
3d5e1f7
to
05e5d8e
Compare
05e5d8e
to
da45be6
Compare
} | ||
|
||
// destructor | ||
CScriptKeyValues::~CScriptKeyValues( ) | ||
{ | ||
if (m_pKeyValues) | ||
// Children are always borrowed | ||
Assert( !m_pBase || m_pSelf->borrow ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember my exact rationale for having CScriptKeyValues
directly wrap the KeyValues
data in logic_externaldata
, although that I may have thought it would be more efficient or intuitive to directly access it. In retrospect, I think that was a poor choice. It may not have occurred to me that the associated data can be removed while the CScriptKeyValues
instance remains.
I like the idea of keeping copies of the script instances to perform proper cleanup while maintaining the current behavior, even if it is cumbersome, although that's not particularly relevant to the rest of this PR and can be resolved another time.
I was just committing the conflict resolution. The SQInteger sq_retval;
if (!sq_isnull(pSquirrelVM->lastError_))
{
sq_pushobject(vm, pSquirrelVM->lastError_);
sq_resetobject(&pSquirrelVM->lastError_);
sq_retval = sq_throwobject(vm);
}
else
{
Assert(script_retval.m_type == pFunc->m_desc.m_ReturnType);
Assert( ( pFunc->m_desc.m_ReturnType != FIELD_VOID ) || !( pFunc->m_flags & SF_REFCOUNTED_RET ) );
if (pFunc->m_desc.m_ReturnType != FIELD_VOID)
{
PushVariant(vm, script_retval);
if ( ( pFunc->m_flags & SF_REFCOUNTED_RET ) && script_retval.m_hScript )
{
Assert( script_retval.m_type == FIELD_HSCRIPT );
// Release the intermediary ref held from RegisterInstance
sq_release(vm, (HSQOBJECT*)script_retval.m_hScript);
delete (HSQOBJECT*)script_retval.m_hScript;
}
sq_retval = 1;
}
else
{
sq_retval = 0;
}
}
// strings never get copied here, Vector and QAngle are stored in script_retval_storage
// everything else is stored inline, so there should be no memory to free
Assert(!(script_retval.m_flags & SV_FREE));
for ( int i = 0; i <= nLastHScriptIdx; ++i )
{
if ( pFunc->m_desc.m_Parameters[i] == FIELD_HSCRIPT )
delete (HSQOBJECT*)params[i].m_hScript;
}
return sq_retval;
} |
Sorry! I should've waited longer after merging z33ky's PR. I wasn't sure if you were going to be around to resolve it on your own. |
FYI, copy-pasting seems to have converted tabs into spaces in my comment 8576fb1#diff-67fd1ca8e9ca5070bbcb6fda805005b7e72c9f5343129ae55103852af548d992R1468 |
Fixes leaks on native function HSCRIPT parameters; adds
IScriptVM::CopyObject
to copy script objecs to be stored in C++. Every HSCRIPT parameter was duplicated, they would never get released until the VM was shutdown.Fixes CScriptKeyValues ownership issues and leaks. Every instance of it was leaking, and it was possible to delete KeyValues not owned by script.
Adds correct way to return reference counted objects to script.
bAllowDestruct
never worked because there was always a reference that was never released.Converted to ref counted instances:
CScriptKeyValues
(FileToKeyValues
),TraceLineComplex
,TraceHullComplex
,CreateDamageInfo
,CreateFireBulletsInfo
Deprecated:
CScriptKeyValues::ReleaseKeyValues
,CGameTrace::Destroy
,DestroyDamageInfo
,DestroyFireBulletsInfo
Does this PR close any issues?
PR Checklist
develop
branch OR targets another branch with a specific goal in mind