Skip to main content
GameDev.net gamedev.net
🔒 Locked

Shared types refcounting bug

Started by Miss Jun 29, 2025 at 12:27 PM 7 replies 2.6k views
Original Post
Miss
Miss

So this one took a bit to narrow down, but I think I have found a bug where the reference count of shared types aren't counted correctly when assigning handles to implemented interfaces.

There's different errors that we've ran into, but for me the most reproducible one is this:

main.as (22, 1) : INFO : Compiling void Test()
main.as (23, 5) :  ERR : No matching signatures to 'Bar::Foo(const int)'
main.as (23, 5) : INFO : Candidates are:
main.as (23, 5) : INFO : void Foo()

Where Foo is another unreferenced shared class. I don't have a minimal reproduction here yet, but the code for Foo is pretty straightforward:

// If `Foo` is not in the `Bar` namespace, the error doesn't reproduce
namespace Bar {
    // `Foo` is only defined and used here
    shared class Foo {
        Foo(int) {}
    }
}

void Test() {
    Bar::Foo(2);
}

Anyway, all the code above by itself is not a problem. It becomes a problem as soon as we assign a handle in another module, and ALSO when another completely unrelated module is unloaded:

// Module A:
shared interface IMedal {}

IMedal@ g_foo;
array<IMedal@> g_foos;

void setMedal(IMedal@ medal) {
    @g_foo = medal; // Without this, everything is fine
    //g_foos.InsertLast(medal); // Interestingly, uncommenting this seems to stop the error again -- potentially this adds an extra reference to the shared type again?
}

// Module B:
shared interface IMedal {}

import void setMedal(IMedal@ medal) from "UME";

class MyMedal : IMedal {}
MyMedal@ g_medal;
void Main() {
    @g_medal = MyMedal();
    setMedal(g_medal);
}

My guess is that the unrelated module unloading is causing some references to be cleared, and because there's not enough references on the shared type, we now get compiler errors because something was freed?

Sorry this bugreport is so vague, I'm still trying to wrap my head around it and get a minimal reproduction :( I'm kinda hoping this might be just enough information to go on 😅

Sometimes we get the error that the declaration is different in another module, even though all modules have been unloaded and reloaded, meaning shared types are definitely leaking somehow. For more context, see: https://github.com/openplanet-nl/issues/issues/451

WitchLord
WitchLord

I will try to reproduce this, but based on your description it definitely sounds like a very complex scenario.

Don't forget that the shared types may also be referenced from objects that are not yet garbage collected.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - <a href="http://www.angelcode.com/tower" rel
WitchLord
WitchLord

I haven't been able to reproduce this yet.

Here's the test case I built for it:

{
	engine = asCreateScriptEngine();
	bout.buffer = "";
	engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);

	RegisterScriptArray(engine, false);

	const char *script1 = // If `Foo` is not in the `Bar` namespace, the error doesn't reproduce
		"namespace Bar {	\n"
		// `Foo` is only defined and used here
		"	shared class Foo { \n"
		"		Foo(int) {} \n"
		"	} \n"
		"} \n"
		"void Test() { \n"
		"	Bar::Foo(2); \n"
		"} \n";

	asIScriptModule* mod = engine->GetModule("test", asGM_ALWAYS_CREATE);
	mod->AddScriptSection("test", script1);
	r = mod->Build();
	if (r < 0)
		TEST_FAILED;

	const char* script2 =
		"shared interface IMedal {} \n"
		"IMedal@ g_foo; \n"
		"array<IMedal@> g_foos; \n"
		"void setMedal(IMedal@ medal) { \n"
		"	@g_foo = medal; \n" // Without this, everything is fine
		"	//g_foos.InsertLast(medal); \n" // Interestingly, uncommenting this seems to stop the error again -- potentially this adds an extra reference to the shared type again?
		"} \n";

	mod = engine->GetModule("testA", asGM_ALWAYS_CREATE);
	mod->AddScriptSection("test", script2);
	r = mod->Build();
	if (r < 0)
		TEST_FAILED;

	const char* script3 =
		"shared interface IMedal {} \n"
		"import void setMedal(IMedal@ medal) from 'testA'; \n"
		"class MyMedal : IMedal {} \n"
		"MyMedal@ g_medal; \n"
		"void Main() { \n"
		"	@g_medal = MyMedal(); \n"
		"	setMedal(g_medal); \n"
		"} \n";

	mod = engine->GetModule("testB", asGM_ALWAYS_CREATE);
	mod->AddScriptSection("test", script3);
	r = mod->Build();
	if (r < 0)
		TEST_FAILED;
	r = mod->BindAllImportedFunctions();
	if (r < 0)
		TEST_FAILED;

	// Execute each of the scripts
	r = ExecuteString(engine, "Test()", engine->GetModule("test"));
	if (r != asEXECUTION_FINISHED) TEST_FAILED;
	asIScriptContext* ctx = engine->CreateContext();
	r = ExecuteString(engine, "Main()", engine->GetModule("testB"), ctx);
	if (r == asEXECUTION_EXCEPTION)
		PRINTF("%s", GetExceptionInfo(ctx).c_str());
	ctx->Release();
	if (r != asEXECUTION_FINISHED) TEST_FAILED;

	// Rebuild module
	engine->DiscardModule("test");
	engine->DiscardModule("testA");
	engine->DiscardModule("testB");

	mod = engine->GetModule("test", asGM_ALWAYS_CREATE);
	mod->AddScriptSection("test", script1);
	r = mod->Build();
	if (r < 0)
		TEST_FAILED;

	engine->ShutDownAndRelease();

	if (bout.buffer != "")
	{
		PRINTF("%s", bout.buffer.c_str());
		TEST_FAILED;
	}
}

Is it intermittent? Or is it something that always happen at the same moment?

Can you perhaps do some debugging to see how the reference count for the shared type is changing throughout time?

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - <a href="http://www.angelcode.com/tower" rel
Miss
Miss

It's consistent, but your tests are not exactly the set up in my case. I've recreated your test code with the accurate steps and it reproduces the problem. Apologies for being different from your regular test code, as I just copypasted it into my existing code to quickly test this, but hopefully you get the idea:

	{
		auto engine = asCreateScriptEngine();
		engine->SetMessageCallback(asFUNCTION(ScriptMessageCallback), nullptr, asCALL_CDECL);

		RegisterScriptArray(engine, false);

		// 1 has a dependency on 2
		const char* script1 =
			"void Main() {}";

		// 2 has a dependency on 3
		const char* script2 =
			"shared interface IMedal {}\n"
			"import void setMedal(IMedal@ medal) from \"test3\";\n"
			"class MyMedal : IMedal {}\n"
			"MyMedal@ g_medal;\n"
			"void Main() {\n"
			"  @g_medal = MyMedal();\n"
			"  setMedal(g_medal);\n"
			"}\n"
			"namespace Bar {\n" // If `Foo` is not in the `Bar` namespace, the error doesn't reproduce
			"  shared class Foo {\n" // `Foo` is only defined and used here
			"    Foo(int) {}\n"
			"  }\n"
			"}\n"
			"void Test() {\n" // Test() is actually never called
			"  Bar::Foo(2);\n"
			"}";

		// 3 has no dependencies
		const char* script3 =
			"shared interface IMedal {}\n"
			"IMedal@ g_foo;\n"
			"void setMedal(IMedal@ medal) {\n"
			"  @g_foo = medal;\n"
			"}";

		asIScriptModule* mod;
		asIScriptContext* ctx;
		int r;

		// Build 3 before 2
		mod = engine->GetModule("test3", asGM_ALWAYS_CREATE);
		mod->AddScriptSection("test3", script3);
		r = mod->Build();
		ASSERT(r >= 0);

		// Build 2 before 1
		mod = engine->GetModule("test2", asGM_ALWAYS_CREATE);
		mod->AddScriptSection("test2", script2);
		r = mod->Build();
		ASSERT(r >= 0);
		r = mod->BindAllImportedFunctions();
		ASSERT(r >= 0);

		// Build 1 last
		mod = engine->GetModule("test1", asGM_ALWAYS_CREATE);
		mod->AddScriptSection("test1", script1);
		r = mod->Build();
		ASSERT(r >= 0);

		// Execute test2 Main()
		mod = engine->GetModule("test2");
		ctx = engine->CreateContext();
		ctx->Prepare(mod->GetFunctionByName("Main"));
		r = ctx->Execute();
		ASSERT(r == asEXECUTION_FINISHED);
		ctx->Release();

		// It's time to reload 2. Since 1 depends on 2, we reload 1 as well
		// We do this twice, as it only happens after the second reload
		for (int i = 0; i < 2; i++) {
			// Unload 1 and 2
			engine->DiscardModule("test1");
			engine->DiscardModule("test2");

			// Rebuild 2 before 1
			mod = engine->GetModule("test2", asGM_ALWAYS_CREATE);
			mod->AddScriptSection("test2", script2);
			r = mod->Build();
			ASSERT(r >= 0); // This assertion will fail on i == 1
			r = mod->BindAllImportedFunctions();
			ASSERT(r >= 0);

			// Rebuild 1 last
			mod = engine->GetModule("test1", asGM_ALWAYS_CREATE);
			mod->AddScriptSection("test1", script1);
			r = mod->Build();

			// Execute test2 Main()
			mod = engine->GetModule("test2");
			ctx = engine->CreateContext();
			ctx->Prepare(mod->GetFunctionByName("Main"));
			r = ctx->Execute();
			ASSERT(r == asEXECUTION_FINISHED);
			ctx->Release();
		}

		engine->ShutDownAndRelease();
	}

The first assertion in the for loop will fail because test2 fails to build:

test2 (14, 1) : INFO : Compiling void Test()
test2 (15, 3) :  ERR : No matching signatures to 'Bar::Foo(const int)'
test2 (15, 3) : INFO : Candidates are:
test2 (15, 3) : INFO : void Foo()
WitchLord
WitchLord

Thanks a lot. With code that reproduces the issue I should be able to identify the cause and fix soon.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - <a href="http://www.angelcode.com/tower" rel
WitchLord
WitchLord

I've fixed this in revision 3058.

https://sourceforge.net/p/angelscript/code/3058/

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - <a href="http://www.angelcode.com/tower" rel
Miss
Miss

Thank you! Do you think it's possible this also could've caused intermittent “doesn't match the original declaration in other module” errors? We haven't been able to find a consistent reproduction for that one yet, unfortunately.

WitchLord
WitchLord

It is possible, though I can't say for sure without it being reproduced. What happened was that the factory functions for the shared class didn't have the ownership transferred to the remaining module and were instead dropped. In a subsequent compilation this might cause the compiler to see a difference with the source code.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - <a href="http://www.angelcode.com/tower" rel

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.