From 81ca95aeb476c25925c33e68ec18f6f9024169ba Mon Sep 17 00:00:00 2001 From: "Enzo Persillet (Tutez)" Date: Thu, 24 Sep 2026 20:32:50 +0200 Subject: [PATCH 1/3] Call a parent toString that takes an optional argument. HX_DEFINE_SCRIPTABLE always calls __superString::toString() with no arguments. A Haxe toString(indent = 0) is one C++ parameter, so that call does not compile. The zero-argument form is used when it exists. Otherwise null is passed for the default. Co-authored-by: Cursor --- include/hx/Scriptable.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/include/hx/Scriptable.h b/include/hx/Scriptable.h index 7a605dc01..0d00f4f8d 100644 --- a/include/hx/Scriptable.h +++ b/include/hx/Scriptable.h @@ -220,6 +220,24 @@ void __Visit(HX_VISIT_PARAMS) HXCPP_OVERRIDE { super::__Visit(HX_VISIT_ARG); ::h #endif +/* + * `toString` with a default argument is one C++ parameter (`hx::Null`), not a + * zero-argument method. Calling `__superString::toString()` then fails to compile. + * The zero-argument form is preferred when it exists; otherwise the default is null. + */ +template +inline auto _hx_scriptable_super_toString(T *inSelf, int) -> decltype(inSelf->T::toString(), ::String()) +{ + return inSelf->T::toString(); +} + +template +inline ::String _hx_scriptable_super_toString(T *inSelf, ...) +{ + return inSelf->T::toString(null()); +} + + #define HX_DEFINE_SCRIPTABLE(ARG_LIST) \ inline void *operator new( size_t inSize, int inExtraDataSize ) \ { \ @@ -235,7 +253,7 @@ void __Visit(HX_VISIT_PARAMS) HXCPP_OVERRIDE { super::__Visit(HX_VISIT_ARG); ::h void ** __GetScriptVTable() HXCPP_OVERRIDE { return __scriptVTable; } \ ::String toString() HXCPP_OVERRIDE { if (__scriptVTable[0] ) \ { ::hx::CppiaCtx *ctx = ::hx::CppiaCtx::getCurrent(); ::hx::AutoStack a(ctx); ctx->pushObject(this); return ctx->runString(__scriptVTable[0]); } \ - else return __superString::toString(); } \ + else return _hx_scriptable_super_toString<__superString>(static_cast<__superString *>(this), 0); } \ ::String __ToString() const HXCPP_OVERRIDE { return ::hx::ScriptableToString(__scriptVTable[-1]); } \ ::hx::Class __GetClass() const HXCPP_OVERRIDE { return ::hx::ScriptableGetClass(__scriptVTable[-1]); } \ int __GetType() const HXCPP_OVERRIDE { return ::hx::ScriptableGetType(__scriptVTable[-1]); } \ From 384fdb80bfdcb82f5f6644b28e757262429b794e Mon Sep 17 00:00:00 2001 From: "Enzo Persillet (Tutez)" Date: Thu, 24 Sep 2026 23:36:10 +0200 Subject: [PATCH 2/3] Compile a scriptable host whose toString takes an optional argument. The cppia host build is what failed: toString(indent = 0) is one C++ parameter, and the scriptable wrapper called it with none. A zero-argument toString is compiled in the same file so that form stays valid. Co-authored-by: Cursor --- test/cppia/OptionalToString.hx | 22 ++++++++++++++++++++++ test/cppia/compile-host.hxml | 1 + 2 files changed, 23 insertions(+) create mode 100644 test/cppia/OptionalToString.hx diff --git a/test/cppia/OptionalToString.hx b/test/cppia/OptionalToString.hx new file mode 100644 index 000000000..d9c0d8b5d --- /dev/null +++ b/test/cppia/OptionalToString.hx @@ -0,0 +1,22 @@ +/** + * A scriptable host whose `toString` takes an optional argument. + * + * hxcpp emits that as `toString(hx::Null)`. The scriptable wrapper used to call + * `toString()` with no arguments, which does not compile. `ZeroArgToString` keeps the + * zero-argument form compiling too. + */ +class OptionalToString { + public function new() {} + + public function toString(indent:Int = 0):String { + return "indent" + indent; + } +} + +class ZeroArgToString { + public function new() {} + + public function toString():String { + return "zero"; + } +} diff --git a/test/cppia/compile-host.hxml b/test/cppia/compile-host.hxml index 76bf2d790..31e6c19e6 100644 --- a/test/cppia/compile-host.hxml +++ b/test/cppia/compile-host.hxml @@ -1,5 +1,6 @@ -m CppiaHost HostExtendedRoot +OptionalToString -D scriptable -D dll_export=host_classes.info -L utest From 95c73b31a45791597ab260f22e9b7df73551f578 Mon Sep 17 00:00:00 2001 From: "Enzo Persillet (Tutez)" Date: Sat, 3 Oct 2026 19:08:30 +0200 Subject: [PATCH 3/3] Fall back on hx::Object::toString for any toString signature. A Haxe toString with parameters compiles to a C++ overload that hides hx::Object::toString() without overriding it, whatever the parameters. Passing null for them only worked for a single optional one. Native code calling toString() on such an object reaches hx::Object::toString(), so the scriptable wrapper now calls that one too. The cppia tests now build host classes that inherit a toString with one optional parameter and with two required ones, and check at run time that script classes extending them print like native ones. Co-Authored-By: Claude Opus 5.5 --- include/hx/Scriptable.h | 10 ++++++---- test/cppia/Client.hx | 7 +++++++ test/cppia/OptionalToString.hx | 20 +++++++++++++++----- test/cppia/cases/TestCommon.hx | 9 +++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/include/hx/Scriptable.h b/include/hx/Scriptable.h index 0d00f4f8d..887a2c3c7 100644 --- a/include/hx/Scriptable.h +++ b/include/hx/Scriptable.h @@ -221,9 +221,11 @@ void __Visit(HX_VISIT_PARAMS) HXCPP_OVERRIDE { super::__Visit(HX_VISIT_ARG); ::h /* - * `toString` with a default argument is one C++ parameter (`hx::Null`), not a - * zero-argument method. Calling `__superString::toString()` then fails to compile. - * The zero-argument form is preferred when it exists; otherwise the default is null. + * A Haxe `toString` with parameters, whatever they are, compiles to a C++ overload that + * hides `hx::Object::toString()` without overriding it. In a class that inherits it, + * `__superString` is that class, so `__superString::toString()` does not compile. + * Native code calling `toString()` on such an object reaches `hx::Object::toString()`, + * so the scriptable wrapper calls that one in this case. */ template inline auto _hx_scriptable_super_toString(T *inSelf, int) -> decltype(inSelf->T::toString(), ::String()) @@ -234,7 +236,7 @@ inline auto _hx_scriptable_super_toString(T *inSelf, int) -> decltype(inSelf->T: template inline ::String _hx_scriptable_super_toString(T *inSelf, ...) { - return inSelf->T::toString(null()); + return inSelf->::hx::Object::toString(); } diff --git a/test/cppia/Client.hx b/test/cppia/Client.hx index 710373f4b..c62c41020 100644 --- a/test/cppia/Client.hx +++ b/test/cppia/Client.hx @@ -14,6 +14,13 @@ class ClientFoo implements IFoo { } } +// None overrides toString: the scriptable wrapper falls back on the host's. +class ClientOptionalToString extends OptionalToString.OptionalToStringChild {} + +class ClientRequiredToString extends OptionalToString.RequiredToStringChild {} + +class ClientZeroArgToString extends OptionalToString.ZeroArgToString {} + class Client { public static var clientBool0 = true; diff --git a/test/cppia/OptionalToString.hx b/test/cppia/OptionalToString.hx index d9c0d8b5d..3c4fc2dac 100644 --- a/test/cppia/OptionalToString.hx +++ b/test/cppia/OptionalToString.hx @@ -1,9 +1,7 @@ /** - * A scriptable host whose `toString` takes an optional argument. - * - * hxcpp emits that as `toString(hx::Null)`. The scriptable wrapper used to call - * `toString()` with no arguments, which does not compile. `ZeroArgToString` keeps the - * zero-argument form compiling too. + * Scriptable hosts whose `toString` takes parameters, which hide `hx::Object::toString()` + * in C++, and one whose `toString` takes none. The children inherit a hiding `toString`, + * so the scriptable wrapper generated for them cannot call `toString()` on its parent. */ class OptionalToString { public function new() {} @@ -13,6 +11,14 @@ class OptionalToString { } } +class RequiredToString { + public function new() {} + + public function toString(indent:Int, prefix:String):String { + return prefix + indent; + } +} + class ZeroArgToString { public function new() {} @@ -20,3 +26,7 @@ class ZeroArgToString { return "zero"; } } + +class OptionalToStringChild extends OptionalToString {} + +class RequiredToStringChild extends RequiredToString {} diff --git a/test/cppia/cases/TestCommon.hx b/test/cppia/cases/TestCommon.hx index eaacb5a76..d97385956 100644 --- a/test/cppia/cases/TestCommon.hx +++ b/test/cppia/cases/TestCommon.hx @@ -59,6 +59,15 @@ class TestCommon extends Test { Assert.equals(2, Common.callbackSet, 'Bad cppia closure'); } + @:depends(testStatus) + function testHostToStringWithParameters() { + // As in native code, a toString with parameters is not the one Std.string calls. + Assert.equals('OptionalToStringChild', Std.string(new OptionalToString.OptionalToStringChild())); + Assert.equals('ClientOptionalToString', Std.string(Type.createInstance(Type.resolveClass('ClientOptionalToString'), []))); + Assert.equals('ClientRequiredToString', Std.string(Type.createInstance(Type.resolveClass('ClientRequiredToString'), []))); + Assert.equals('zero', Std.string(Type.createInstance(Type.resolveClass('ClientZeroArgToString'), []))); + } + @:depends(testStatus) function testInterfaceCalling() { final obj : IFoo = Type.createInstance(Type.resolveClass('ClientFoo'), []);