From 222cc06e29e091710bb73638967509160069de51 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Mon, 25 Aug 2025 11:41:51 +0100 Subject: [PATCH 1/4] Avoid duplicate string iteration in TCopyString TConvertToUTF8 treats strings as null terminating if the length is 0, so rather than calculating the size manually we can set it to 0 if it the input to TCopyString is -1. For GCStringDup, -1 is handled as null terminating, but 0 is treated as a 0 length string. We can pass the length argument straight in without modification there. This avoids duplicate loops in both the TConvertToUTF8 and GCStringDup cases. --- include/hxString.h | 1 + src/String.cpp | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/include/hxString.h b/include/hxString.h index 9e82f2469..3ad04366c 100644 --- a/include/hxString.h +++ b/include/hxString.h @@ -42,6 +42,7 @@ class HXCPP_EXTERN_CLASS_ATTRIBUTES String inline String(const char16_t *inPtr) { *this = create(inPtr); } inline String(const char *inPtr) { *this = create(inPtr); } + // If inLen is -1, the input string is treated as null terminated. static String create(const wchar_t *inPtr,int inLen=-1); static String create(const char16_t *inPtr,int inLen=-1); static String create(const char *inPtr,int inLen=-1); diff --git a/src/String.cpp b/src/String.cpp index fedef3873..0d59bb05a 100644 --- a/src/String.cpp +++ b/src/String.cpp @@ -446,9 +446,6 @@ inline String TCopyString(const T *inString,int inLength) return String(); #ifndef HX_SMART_STRINGS - if (inLength<0) - for(inLength=0; !inString[inLength]; inString++) { } - if (sizeof(T)==1) { int len = 0; @@ -457,7 +454,7 @@ inline String TCopyString(const T *inString,int inLength) } else { - int length = inLength; + int length = inLength > 0 ? inLength : 0; const char *ptr = TConvertToUTF8(inString, &length, 0, true ); return String(ptr,length); } From 7911676a42b064f49d2f5e3cd64119b517d14574 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 26 Aug 2025 18:14:36 +0100 Subject: [PATCH 2/4] Add test for String::create unspecified length --- .github/workflows/test.yml | 16 ++++++++ test/regression/Issue849/Main.hx | 10 +++++ test/regression/Issue849/build.hxml | 3 ++ test/regression/Issue849/stdout.txt | 3 ++ test/regression/Run.hx | 64 +++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 test/regression/Issue849/Main.hx create mode 100644 test/regression/Issue849/build.hxml create mode 100644 test/regression/Issue849/stdout.txt create mode 100644 test/regression/Run.hx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e3ddbf6d..2289108c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -221,3 +221,19 @@ jobs: run: haxe compile-cpp.hxml -D ${{ env.HXCPP_ARCH_FLAG }} -D no_http - name: run run: bin${{ inputs.sep }}cpp${{ inputs.sep }}TestMain-debug + + regression: + runs-on: ${{ inputs.os }} + name: regression tests + defaults: + run: + working-directory: test/regression + steps: + - name: checkout + uses: actions/checkout@v4 + - name: setup + uses: ./.github/workflows/setup + with: + haxe: ${{ inputs.haxe }} + - name: run + run: haxe --run Run -D ${{ env.HXCPP_ARCH_FLAG }} diff --git a/test/regression/Issue849/Main.hx b/test/regression/Issue849/Main.hx new file mode 100644 index 000000000..39af8d2ef --- /dev/null +++ b/test/regression/Issue849/Main.hx @@ -0,0 +1,10 @@ +function main() { + // char + trace(untyped __cpp__('::String::create("Hello world")')); + + // wchar_t + trace(untyped __cpp__('::String::create(L"Hello world")')); + + // char16_t + trace(untyped __cpp__('::String::create(u"Hello world")')); +} diff --git a/test/regression/Issue849/build.hxml b/test/regression/Issue849/build.hxml new file mode 100644 index 000000000..542c501e0 --- /dev/null +++ b/test/regression/Issue849/build.hxml @@ -0,0 +1,3 @@ +-cpp bin +-D disable-unicode-strings +-m Main diff --git a/test/regression/Issue849/stdout.txt b/test/regression/Issue849/stdout.txt new file mode 100644 index 000000000..d3bf90c6f --- /dev/null +++ b/test/regression/Issue849/stdout.txt @@ -0,0 +1,3 @@ +Main.hx:3: Hello world +Main.hx:6: Hello world +Main.hx:9: Hello world diff --git a/test/regression/Run.hx b/test/regression/Run.hx new file mode 100644 index 000000000..40b5a045e --- /dev/null +++ b/test/regression/Run.hx @@ -0,0 +1,64 @@ +import sys.io.Process; +import sys.io.File; +import sys.FileSystem; + +using StringTools; + +function runOutput(test:String):String { + final slash = Sys.systemName() == "Windows" ? "\\" : "/"; + final proc = new Process([test, "bin", 'Main'].join(slash)); + final code = proc.exitCode(); + + if (code != 0) { + throw 'return code was $code'; + } + + return proc.stdout.readAll().toString().replace("\r\n", "\n"); +} + +function main() { + var successes = 0; + var total = 0; + + final args = Sys.args(); + + for (test in FileSystem.readDirectory(".")) { + if (!FileSystem.isDirectory(test)) { + continue; + } + + total++; + + final buildExitCode = Sys.command("haxe", ["-C", test, "build.hxml"].concat(args)); + if (buildExitCode != 0) { + Sys.println('Failed to build test $test. Exit code: $buildExitCode'); + continue; + } + + final expectedStdout = File.getContent('$test/stdout.txt').replace("\r\n", "\n"); + final actualStdout = try { + runOutput(test); + } catch (e) { + Sys.println('Test $test failed: $e'); + continue; + }; + + if (actualStdout != expectedStdout) { + Sys.println('Test $test failed: Output did not match'); + + Sys.println("Expected stdout:"); + Sys.println(expectedStdout); + Sys.println("Actual stdout:"); + Sys.println(actualStdout); + continue; + } + + successes++; + } + + Sys.println('Regression tests complete. Successes: $successes / $total'); + + if (successes < total) { + Sys.exit(1); + } +} From 1b50266bf4f06d9b44e31199cc70c46dddc411bf Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Wed, 27 Aug 2025 18:59:06 +0100 Subject: [PATCH 3/4] Fix String::create(_, 0) without HX_SMART_STRINGS When smart strings are enabled, this always returns an empty string, however, if they are disabled this currently has different behaviour depending on the type of character passed in. This fixes the behaviour so that it also returns an empty string for wchar_t and char16_t strings when smart strings are disabled. --- src/String.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/String.cpp b/src/String.cpp index 0d59bb05a..7f0836f52 100644 --- a/src/String.cpp +++ b/src/String.cpp @@ -454,6 +454,9 @@ inline String TCopyString(const T *inString,int inLength) } else { + if (inLength == 0) { + return String::emptyString; + } int length = inLength > 0 ? inLength : 0; const char *ptr = TConvertToUTF8(inString, &length, 0, true ); return String(ptr,length); From b506ac904c8554059ffa3f204c159debc80c5a5b Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Wed, 27 Aug 2025 19:03:40 +0100 Subject: [PATCH 4/4] Add checks for String::create with length 0 --- test/regression/Issue849/Main.hx | 11 +++++++++++ test/regression/Issue849/stdout.txt | 3 +++ 2 files changed, 14 insertions(+) diff --git a/test/regression/Issue849/Main.hx b/test/regression/Issue849/Main.hx index 39af8d2ef..f2e758d2f 100644 --- a/test/regression/Issue849/Main.hx +++ b/test/regression/Issue849/Main.hx @@ -7,4 +7,15 @@ function main() { // char16_t trace(untyped __cpp__('::String::create(u"Hello world")')); + + // explicit 0 length + + // char + trace(untyped __cpp__('::String::create("Hello world", 0)')); + + // wchar_t + trace(untyped __cpp__('::String::create(L"Hello world", 0)')); + + // char16_t + trace(untyped __cpp__('::String::create(u"Hello world", 0)')); } diff --git a/test/regression/Issue849/stdout.txt b/test/regression/Issue849/stdout.txt index d3bf90c6f..c1214794c 100644 --- a/test/regression/Issue849/stdout.txt +++ b/test/regression/Issue849/stdout.txt @@ -1,3 +1,6 @@ Main.hx:3: Hello world Main.hx:6: Hello world Main.hx:9: Hello world +Main.hx:14: +Main.hx:17: +Main.hx:20: