diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 6a6eb6abbc..962e57ae8e 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -21,6 +21,8 @@ #include "java_lang_Float.h" #include "java_lang_Runnable.h" #include "java_lang_Throwable.h" +#include "java_lang_StackOverflowError.h" +#include "java_lang_VirtualMachineError.h" #include "java_lang_StringBuilder.h" #include "java_util_HashMap.h" #include "java_util_HashMap_Entry.h" @@ -1550,9 +1552,14 @@ void initMethodStack(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, int THROW_NULL_POINTER_EXCEPTION(); } #endif + if (threadStateData->callStackOffset >= CN1_MAX_STACK_CALL_DEPTH - 1) { + JAVA_OBJECT stackOverflow = __NEW_INSTANCE_java_lang_StackOverflowError(threadStateData); + java_lang_Throwable_fillInStack__(threadStateData, stackOverflow); + throwException(threadStateData, stackOverflow); + return; + } memset(&threadStateData->threadObjectStack[threadStateData->threadObjectStackOffset], 0, sizeof(struct elementStruct) * (localsStackSize + stackSize)); threadStateData->threadObjectStackOffset += localsStackSize + stackSize; - CODENAME_ONE_ASSERT(threadStateData->callStackOffset < CN1_MAX_STACK_CALL_DEPTH - 1); threadStateData->callStackClass[threadStateData->callStackOffset] = classNameId; threadStateData->callStackMethod[threadStateData->callStackOffset] = methodNameId; threadStateData->callStackOffset++; diff --git a/vm/JavaAPI/src/java/lang/StackOverflowError.java b/vm/JavaAPI/src/java/lang/StackOverflowError.java new file mode 100644 index 0000000000..5b6913a931 --- /dev/null +++ b/vm/JavaAPI/src/java/lang/StackOverflowError.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.lang; + +/** + * Thrown when a stack overflow occurs because an application recurses too deeply. + * Since: JDK1.0, CLDC 1.0 + */ +public class StackOverflowError extends VirtualMachineError { + /** + * Constructs a StackOverflowError with no detail message. + */ + public StackOverflowError() { + } + + /** + * Constructs a StackOverflowError with the specified detail message. + * @param s the detail message. + */ + public StackOverflowError(String s) { + super(s); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java index 4a55df8d05..608562fc7e 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java @@ -142,13 +142,13 @@ void translatesOptimizedBytecodeToLLVMExecutable(CompilerHelper.CompilerConfig c Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); @@ -293,13 +293,13 @@ void translatesInvokeAndLdcBytecodeToLLVMExecutable(CompilerHelper.CompilerConfi Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index 78f148ea92..7eeb3835a1 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -7,6 +7,7 @@ import javax.tools.JavaCompiler; import javax.tools.ToolProvider; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; @@ -100,17 +101,18 @@ void generatesRunnableHelloWorldUsingCleanTarget(CompilerHelper.CompilerConfig c writeRuntimeStubs(srcRoot); replaceLibraryWithExecutableTarget(cmakeLists, srcRoot.getFileName().toString()); + relaxLiteralRangeWarnings(cmakeLists); Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - runCommand(Arrays.asList( + List cmakeCommand = new java.util.ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(cmakeCompilerArgs()); + runCommand(cmakeCommand, distDir); runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); @@ -188,6 +190,71 @@ static void replaceLibraryWithExecutableTarget(Path cmakeLists, String sourceDir Files.write(cmakeLists, replacement.getBytes(StandardCharsets.UTF_8)); } + static void relaxLiteralRangeWarnings(Path cmakeLists) throws IOException { + final String marker = "CN1_LITERAL_RANGE_FLAGS"; + String content = new String(Files.readAllBytes(cmakeLists), StandardCharsets.UTF_8); + if (content.contains(marker)) { + return; + } + + String newline = "\n"; + StringBuilder block = new StringBuilder(); + block.append(newline) + .append("# CN1 literal-range warning relaxation").append(newline) + .append("set(").append(marker).append(" \"-Wno-error=literal-range -Wno-literal-range\")").append(newline) + .append("if (CMAKE_C_COMPILER_ID MATCHES \"Clang\")").append(newline) + .append(" set(CMAKE_C_FLAGS \"${CMAKE_C_FLAGS} ${").append(marker).append("}\")").append(newline) + .append("endif").append(newline) + .append("if (CMAKE_OBJC_COMPILER_ID MATCHES \"Clang\")").append(newline) + .append(" set(CMAKE_OBJC_FLAGS \"${CMAKE_OBJC_FLAGS} ${").append(marker).append("}\")").append(newline) + .append("endif").append(newline); + + StringBuilder amended = new StringBuilder(content); + if (!content.endsWith("\n") && !content.endsWith("\r\n")) { + amended.append(newline); + } + amended.append(block); + + Files.write(cmakeLists, amended.toString().getBytes(StandardCharsets.UTF_8)); + } + + static List cmakeCompilerArgs() { + List args = new java.util.ArrayList<>(); + String cCompiler = findExecutable(Arrays.asList("clang", "cc", "gcc")); + if (cCompiler != null) { + args.add("-DCMAKE_C_COMPILER=" + cCompiler); + } + String objcCompiler = findExecutable(Arrays.asList("clang", "gcc", "cc")); + if (objcCompiler != null) { + args.add("-DCMAKE_OBJC_COMPILER=" + objcCompiler); + } + return args; + } + + private static String findExecutable(List candidates) { + for (String candidate : candidates) { + Path found = findOnPath(candidate); + if (found != null) { + return found.toString(); + } + } + return null; + } + + private static Path findOnPath(String executable) { + String path = System.getenv("PATH"); + if (path == null) { + return null; + } + for (String dir : path.split(File.pathSeparator)) { + Path candidate = Paths.get(dir, executable); + if (Files.isExecutable(candidate)) { + return candidate; + } + } + return null; + } + static String runCommand(List command, Path workingDir) throws Exception { ProcessBuilder builder = new ProcessBuilder(command); builder.directory(workingDir.toFile()); @@ -202,6 +269,29 @@ static String runCommand(List command, Path workingDir) throws Exception return output; } + static CommandResult runCommandWithResult(List command, Path workingDir) throws Exception { + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(workingDir.toFile()); + builder.redirectErrorStream(true); + Process process = builder.start(); + String output; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + output = reader.lines().collect(Collectors.joining("\n")); + } + int exit = process.waitFor(); + return new CommandResult(exit, output); + } + + static final class CommandResult { + final int exitCode; + final String output; + + CommandResult(int exitCode, String output) { + this.exitCode = exitCode; + this.output = output; + } + } + static void patchCn1Globals(Path srcRoot) throws IOException { Path cn1Globals = srcRoot.resolve("cn1_globals.h"); String content = new String(Files.readAllBytes(cn1Globals), StandardCharsets.UTF_8); @@ -215,6 +305,22 @@ static void patchCn1Globals(Path srcRoot) throws IOException { } } + static void patchStaticGetterPrototypes(Path srcRoot) throws IOException { + Files.walk(srcRoot) + .filter(p -> p.toString().endsWith(".h")) + .forEach(p -> { + try { + String original = new String(Files.readAllBytes(p), StandardCharsets.UTF_8); + String patched = original.replaceAll("(get_static_[A-Za-z0-9_]+)\\(\\);", "$1(CODENAME_ONE_THREAD_STATE);"); + if (!original.equals(patched)) { + Files.write(p, patched.getBytes(StandardCharsets.UTF_8)); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + static void writeRuntimeStubs(Path srcRoot) throws IOException { Path objectHeader = srcRoot.resolve("java_lang_Object.h"); if (!Files.exists(objectHeader)) { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java index 39864a0872..5a7a2bfdd6 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -136,7 +137,17 @@ public String toString() { } } - public static boolean compileAndRun(String code, String expectedOutput) throws Exception { + public static class ExecutionResult { + public final int exitCode; + public final String output; + + public ExecutionResult(int exitCode, String output) { + this.exitCode = exitCode; + this.output = output; + } + } + + public static ExecutionResult compileAndRunForResult(String code) throws Exception { // Find a suitable compiler (e.g. JDK 8 targeting 1.8) List compilers = getAvailableCompilers("1.8"); if (compilers.isEmpty()) { @@ -170,7 +181,7 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E compileArgs.add(sourceDir.resolve("Main.java").toString()); if (compile(config.jdkHome, compileArgs) != 0) { - return false; + return new ExecutionResult(-1, "javac failed"); } // Merge javaApiDir into classesDir so translator finds dependencies @@ -195,6 +206,7 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E java.nio.file.Path distDir = outputDir.resolve("dist"); java.nio.file.Path srcRoot = distDir.resolve("ExecutorApp-src"); CleanTargetIntegrationTest.patchCn1Globals(srcRoot); + CleanTargetIntegrationTest.patchStaticGetterPrototypes(srcRoot); // Write basic stubs java.nio.file.Path ioFileHeader = srcRoot.resolve("java_io_File.h"); @@ -210,12 +222,15 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E "#endif\n"; java.nio.file.Files.write(objectHeader, headerContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); } - java.nio.file.Path stubs = srcRoot.resolve("runtime_stubs.c"); - String content = "#include \"cn1_globals.h\"\n" + - "#include \n" + - "#include \n" + - "#include \n" + - "#include \n" + + java.nio.file.Path stubs = srcRoot.resolve("runtime_stubs.c"); + String content = "#include \"cn1_globals.h\"\n" + + "#include \"java_lang_StackOverflowError.h\"\n" + + "#include \"java_lang_Throwable.h\"\n" + + "#include \n" + + "#include \n" + + "#include \n" + + "#include \n" + + "#include \n" + "\n" + "struct my_java_lang_String {\n" + " JAVA_OBJECT __codenameOneParentClsReference;\n" + @@ -320,11 +335,24 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E "}\n" + "\n" + "void initMethodStack(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT __cn1ThisObject, int stackSize, int localsStackSize, int classNameId, int methodNameId) {\n" + - " threadStateData->threadObjectStackOffset += localsStackSize;\n" + + " if (threadStateData->callStackOffset >= CN1_MAX_STACK_CALL_DEPTH - 1) {\n" + + " JAVA_OBJECT stackOverflow = __NEW_INSTANCE_java_lang_StackOverflowError(threadStateData);\n" + + " java_lang_Throwable_fillInStack__(threadStateData, stackOverflow);\n" + + " throwException(threadStateData, stackOverflow);\n" + + " return;\n" + + " }\n" + + " memset(&threadStateData->threadObjectStack[threadStateData->threadObjectStackOffset], 0, sizeof(struct elementStruct) * (localsStackSize + stackSize));\n" + + " threadStateData->threadObjectStackOffset += localsStackSize + stackSize;\n" + + " threadStateData->callStackClass[threadStateData->callStackOffset] = classNameId;\n" + + " threadStateData->callStackMethod[threadStateData->callStackOffset] = methodNameId;\n" + + " threadStateData->callStackOffset++;\n" + "}\n" + "\n" + "void releaseForReturn(CODENAME_ONE_THREAD_STATE, int cn1LocalsBeginInThread) {\n" + " threadStateData->threadObjectStackOffset = cn1LocalsBeginInThread;\n" + + " if (threadStateData->callStackOffset > 0) {\n" + + " threadStateData->callStackOffset--;\n" + + " }\n" + "}\n" + "\n" + "void releaseForReturnInException(CODENAME_ONE_THREAD_STATE, int cn1LocalsBeginInThread, int methodBlockOffset) {\n" + @@ -351,9 +379,15 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E "}\n" + "\n" + "void throwException(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj) {\n" + - " (void)obj;\n" + + " if (obj != JAVA_NULL && obj->__codenameOneParentClsReference != 0 && obj->__codenameOneParentClsReference->clsName != NULL) {\n" + + " fprintf(stderr, \"Exception thrown: %s\\n\", obj->__codenameOneParentClsReference->clsName);\n" + + " } else {\n" + + " fprintf(stderr, \"Exception thrown: %p\\n\", obj);\n" + + " }\n" + + " fflush(stderr);\n" + " exit(1);\n" + "}\n" + + "void java_lang_Throwable_fillInStack__(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT me) { (void)threadStateData; (void)me; }\n" + "\n" + "void gcMarkObject(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj, JAVA_BOOLEAN force) { (void)obj; (void)force; }\n" + "// Stub instanceofFunction. Note: signature in cn1_globals.h might differ (int vs pointers) in some versions.\n" + @@ -527,31 +561,38 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E java.nio.file.Files.write(stubs, content.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(outputDir.resolve("dist").resolve("CMakeLists.txt"), "ExecutorApp-src"); + java.nio.file.Path cmakeLists = outputDir.resolve("dist").resolve("CMakeLists.txt"); + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(cmakeLists, "ExecutorApp-src"); + CleanTargetIntegrationTest.relaxLiteralRangeWarnings(cmakeLists); java.nio.file.Path buildDir = distDir.resolve("build"); java.nio.file.Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); java.nio.file.Path executable = buildDir.resolve("ExecutorApp"); - String output = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); - return output.contains(expectedOutput); + CleanTargetIntegrationTest.CommandResult result = CleanTargetIntegrationTest.runCommandWithResult(Arrays.asList(executable.toString()), buildDir); + return new ExecutionResult(result.exitCode, result.output); } finally { // cleanup? } } - private static void compileJavaAPI(java.nio.file.Path outputDir) throws IOException, InterruptedException { + public static boolean compileAndRun(String code, String expectedOutput) throws Exception { + ExecutionResult result = compileAndRunForResult(code); + return result.exitCode == 0 && result.output.contains(expectedOutput); + } + + static void compileJavaAPI(java.nio.file.Path outputDir) throws IOException, InterruptedException { java.nio.file.Files.createDirectories(outputDir); java.nio.file.Path javaApiRoot = java.nio.file.Paths.get("..", "JavaAPI", "src").normalize().toAbsolutePath(); List sources = new ArrayList<>(); @@ -571,4 +612,5 @@ private static void compileJavaAPI(java.nio.file.Path outputDir) throws IOExcept compiler.run(null, null, null, args.toArray(new String[0])); } + } diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/FileClassIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/FileClassIntegrationTest.java index d358ae4244..b84322d93e 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/FileClassIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/FileClassIntegrationTest.java @@ -81,22 +81,24 @@ public void testFileClassMethods(CompilerHelper.CompilerConfig config) throws Ex Path srcRoot = distDir.resolve("FileTestApp-src"); CleanTargetIntegrationTest.patchCn1Globals(srcRoot); + CleanTargetIntegrationTest.patchStaticGetterPrototypes(srcRoot); // Ensure java_io_File.m is included (ByteCodeTranslator should copy it) assertTrue(Files.exists(srcRoot.resolve("java_io_File.m")), "java_io_File.m should exist"); replaceLibraryWithExecutableTarget(cmakeLists, srcRoot.getFileName().toString()); + CleanTargetIntegrationTest.relaxLiteralRangeWarnings(cmakeLists); Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/LambdaIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/LambdaIntegrationTest.java index f9f14e931f..114c15cc5e 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/LambdaIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/LambdaIntegrationTest.java @@ -128,13 +128,13 @@ void translatesLambdaBytecodeToLLVMExecutable(String targetVersion) throws Excep Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java index 45c50c6fd4..8c47bffd8c 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/LockIntegrationTest.java @@ -95,13 +95,13 @@ void verifiesLockAndReentrantLockBehavior(CompilerHelper.CompilerConfig config) Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new java.util.ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java index 054e5e1cfd..eb970138dd 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/ReadWriteLockIntegrationTest.java @@ -99,13 +99,13 @@ void verifiesReadWriteLockBehavior(CompilerHelper.CompilerConfig config) throws Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new java.util.ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowErrorCompilationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowErrorCompilationTest.java new file mode 100644 index 0000000000..7faa769731 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StackOverflowErrorCompilationTest.java @@ -0,0 +1,70 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StackOverflowErrorCompilationTest { + + @Test + void javaApiBuildProducesStackOverflowAndVirtualMachineErrors() throws Exception { + Path outputDir = Files.createTempDirectory("java-api-classes"); + + CompilerHelper.compileJavaAPI(outputDir); + + Path vmErrorClass = outputDir.resolve("java/lang/VirtualMachineError.class"); + Path stackOverflowClass = outputDir.resolve("java/lang/StackOverflowError.class"); + + assertTrue(Files.exists(vmErrorClass), "VirtualMachineError class should be generated"); + assertTrue(Files.exists(stackOverflowClass), "StackOverflowError class should be generated"); + + ClassReader vmErrorReader = new ClassReader(Files.readAllBytes(vmErrorClass)); + assertEquals("java/lang/Error", vmErrorReader.getSuperName(), + "VirtualMachineError should extend Error"); + + ClassReader stackOverflowReader = new ClassReader(Files.readAllBytes(stackOverflowClass)); + assertEquals("java/lang/VirtualMachineError", stackOverflowReader.getSuperName(), + "StackOverflowError should extend VirtualMachineError"); + } + + @Test + void translatedRuntimeRaisesStackOverflowOnDeepRecursion() throws Exception { + String code = "package test;\n" + + "public class Main {\n" + + " private static native void print(String s);\n" + + " static long ping(int depth, long salt) {\n" + + " long guard = salt ^ depth;\n" + + " byte[] pad = new byte[32];\n" + + " pad[(depth & 7)] = (byte)guard;\n" + + " if (depth <= 0) return guard + pad[0];\n" + + " long branch = pong(depth - 1, guard + pad[0]);\n" + + " return branch + pad[1] + guard;\n" + + " }\n" + + " static long pong(int depth, long salt) {\n" + + " long shuffle = salt + depth;\n" + + " return ping(depth - 1, shuffle) ^ shuffle;\n" + + " }\n" + + " public static void main(String[] args) {\n" + + " try {\n" + + " print(String.valueOf(ping(1600, System.nanoTime())));\n" + + " } catch (StackOverflowError err) {\n" + + " print(\"Caught: \" + err.getClass().getName());\n" + + " print(err.toString());\n" + + " }\n" + + " }\n" + + "}\n"; + + CompilerHelper.ExecutionResult result = CompilerHelper.compileAndRunForResult(code); + + assertTrue(result.exitCode != 0, "Translated binary should surface the overflow instead of running to completion"); + assertNotEquals(139, result.exitCode, "Stack overflow should be surfaced as an exception, not a crash"); + assertTrue(result.output.contains("StackOverflowError"), + () -> "Runtime output should mention StackOverflowError\nOutput:\n" + result.output); + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java index 396743994d..efd7718b38 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StampedLockIntegrationTest.java @@ -96,13 +96,13 @@ void verifiesStampedLockBehavior(CompilerHelper.CompilerConfig config) throws Ex Path buildDir = distDir.resolve("build"); Files.createDirectories(buildDir); - CleanTargetIntegrationTest.runCommand(Arrays.asList( + List cmakeCommand = new java.util.ArrayList<>(Arrays.asList( "cmake", "-S", distDir.toString(), - "-B", buildDir.toString(), - "-DCMAKE_C_COMPILER=clang", - "-DCMAKE_OBJC_COMPILER=clang" - ), distDir); + "-B", buildDir.toString() + )); + cmakeCommand.addAll(CleanTargetIntegrationTest.cmakeCompilerArgs()); + CleanTargetIntegrationTest.runCommand(cmakeCommand, distDir); CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir);