From 69b35dc8b4807bf296a5473eaf83cb944b8c4bdf 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 431a44745..799eb61a0 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 d7a5071d7..b521f5415 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 275717fb9559d80b5f7438e8a47933575d2987c4 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 | 17 ++++++++ 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, 97 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 97fe0a7f0..3eac4e98d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -237,6 +237,7 @@ 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 + build_tool: runs-on: ${{ inputs.os }} name: build tool tests @@ -255,3 +256,19 @@ jobs: haxelib run hxcpp $xml haxelib run hxcpp $xml clean done + + 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 43ac9ff367ed41e07a13f8fa5264b75400f301e9 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 b521f5415..91db46b57 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 72b120790ff61645a3853fe9ad6e7455d3625a74 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: