Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/guides/java-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,52 @@ try {
}
```

### Handling Script Exit

When a Perl script calls `exit()`, PerlOnJava throws a `PerlExitException` instead of
terminating the JVM. This allows your Java application to handle script completion
gracefully and continue execution.

Note: Like in standard Perl, `exit()` is not caught by Perl's `eval{}` blocks - it
always propagates to the Java caller.

```java
import org.perlonjava.runtime.runtimetypes.PerlExitException;

ScriptEngine engine = manager.getEngineByName("perl");

try {
engine.eval("print 'Processing...'; exit 0;");
} catch (ScriptException e) {
if (e.getCause() instanceof PerlExitException exitEx) {
System.out.println("Script exited with code: " + exitEx.getExitCode());
// Continue with other work...
} else {
throw e;
}
}
```

This is particularly important when running scripts like ExifTool that call `exit()`
after completing their work:

```java
import org.perlonjava.app.cli.ArgumentParser;
import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.runtime.runtimetypes.PerlExitException;

String[] scriptArgs = new String[]{"path/to/exiftool", "-ver"};
CompilerOptions options = ArgumentParser.parseArguments(scriptArgs);

try {
PerlLanguageProvider.executePerlCode(options, true);
} catch (PerlExitException e) {
System.out.println("ExifTool exited with code: " + e.getExitCode());
// Script completed successfully, continue processing...
}
```

## Using PerlOnJava Directly

For more control, you can use PerlOnJava's internal API directly.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/perlonjava/app/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.PerlExitException;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;

import java.util.Locale;
Expand Down Expand Up @@ -49,6 +50,9 @@ public static void main(String[] args) {
if (rawChildStatus > 0 && rawChildStatus <= 255) {
System.exit(rawChildStatus);
}
} catch (PerlExitException e) {
// Perl's exit() throws PerlExitException - convert to real System.exit() for CLI
System.exit(e.getExitCode());
} catch (Throwable t) {
if (parsedArgs.debugEnabled) {
// Print full JVM stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c
System.out.println(errorMessage);
System.out.println("END failed--call queue aborted.");
}
} catch (PerlExitException e) {
// PerlExitException already ran END blocks and closed handles in WarnDie.exit()
// Just re-throw for the caller to handle
throw e;
} catch (Throwable t) {
if (isMainProgram) {
runEndBlocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
String javaLine = (st.length > 0) ? " [java:" + st[0].getFileName() + ":" + st[0].getLineNumber() + "]" : "";
String errorMessage = "ClassCastException" + bcContext + ": " + e.getMessage() + javaLine;
throw new RuntimeException(formatInterpreterError(code, errorPc, new Exception(errorMessage)), e);
} catch (PerlExitException e) {
// exit() should NEVER be caught by eval{} - always propagate
throw e;
} catch (Throwable e) {
// Check if we're inside an eval block
if (!evalCatchStack.isEmpty()) {
Expand All @@ -1604,9 +1607,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
}

// Not in eval block - propagate exception
// If it's already a PerlDieException, re-throw as-is for proper formatting
if (e instanceof PerlDieException) {
throw (PerlDieException) e;
// Re-throw RuntimeExceptions as-is (includes PerlDieException)
if (e instanceof RuntimeException re) {
throw re;
}

// Check if we're running inside an eval STRING context
Expand Down
26 changes: 19 additions & 7 deletions src/main/java/org/perlonjava/runtime/operators/WarnDie.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ private static Throwable unwrapException(Throwable throwable) {
}

/**
* Catches the exception in an eval-block
* Catches the exception in an eval-block.
* Note: PerlExitException should NEVER be caught by eval{} - it always propagates.
*/
public static RuntimeScalar catchEval(Throwable e) {
e = unwrapException(e);

// exit() should never be caught by eval{} - re-throw it
if (e instanceof PerlExitException) {
throw (PerlExitException) e;
}

RuntimeScalar err = getGlobalVariable("main::@");

if (e instanceof PerlDieException pde) {
Expand Down Expand Up @@ -276,23 +283,28 @@ private static RuntimeBase dieEmptyMessage(RuntimeScalar oldErr, String fileName
}

/**
* Terminates the program
* Terminates the program by throwing PerlExitException.
* <p>
* This allows embedded/library use where the calling Java application
* can catch the exception and continue execution. The CLI (Main.main())
* catches this and converts it to a real System.exit() call.
*
* @param runtimeScalar with exit status
* @return nothing
* @return nothing (always throws)
* @throws PerlExitException always thrown with the exit code
*/
public static RuntimeScalar exit(RuntimeScalar runtimeScalar) {
int exitCode = runtimeScalar.getInt();
try {
runEndBlocks();
RuntimeIO.closeAllHandles();
} catch (Throwable t) {
RuntimeIO.closeAllHandles();
String errorMessage = ErrorMessageUtil.stringifyException(t);
System.err.println(errorMessage);
System.exit(1);
throw new PerlExitException(1);
}
System.exit(runtimeScalar.getInt());
return new RuntimeScalar(); // This line will never be reached
RuntimeIO.closeAllHandles();
throw new PerlExitException(exitCode);
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/org/perlonjava/runtime/perlmodule/JavaSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,17 @@ public static RuntimeList gc(RuntimeArray args, int ctx) {
}

/**
* Exit the JVM
* Exit the JVM by throwing PerlExitException.
* <p>
* This allows embedded/library use where the calling Java application
* can catch the exception and continue execution.
* Usage: exit(0);
*
* @throws PerlExitException always thrown with the exit code
*/
public static RuntimeList exit(RuntimeArray args, int ctx) {
int status = args.size() > 0 ? (int) args.get(0).getLong() : 0;
System.exit(status);
return scalarUndef.getList(); // Never reached
throw new PerlExitException(status);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.perlonjava.runtime.runtimetypes;

import java.io.Serial;

/**
* Exception used to implement Perl's exit() semantics for embedded/library use.
* <p>
* When Perl code calls exit(), this exception is thrown instead of calling
* System.exit(), allowing the calling Java application to handle the exit
* gracefully and continue execution.
* <p>
* The CLI (Main.main()) catches this exception and converts it to a real
* System.exit() call, while library users can catch it and handle the exit
* code as needed.
*/
public class PerlExitException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;

private final int exitCode;

public PerlExitException(int exitCode) {
super("exit " + exitCode);
this.exitCode = exitCode;
}

public int getExitCode() {
return exitCode;
}
}
16 changes: 10 additions & 6 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1965,10 +1965,12 @@ public RuntimeList apply(RuntimeArray a, int callContext) {
}
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (!(targetException instanceof RuntimeException)) {
throw new RuntimeException(targetException);
if (targetException instanceof RuntimeException re) {
throw re;
}
throw (RuntimeException) targetException;
throw new RuntimeException(targetException);
} catch (RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
Expand Down Expand Up @@ -2037,10 +2039,12 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext)
}
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (!(targetException instanceof RuntimeException)) {
throw new RuntimeException(targetException);
if (targetException instanceof RuntimeException re) {
throw re;
}
throw (RuntimeException) targetException;
throw new RuntimeException(targetException);
} catch (RuntimeException e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
Expand Down
104 changes: 104 additions & 0 deletions src/test/java/org/perlonjava/PerlExitExceptionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.perlonjava;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.runtime.runtimetypes.PerlExitException;

import static org.junit.jupiter.api.Assertions.*;

/**
* Test that PerlExitException is thrown when Perl code calls exit(),
* allowing library users to catch it and continue execution.
*
* This addresses GitHub issue #291 where executePerlCode() was
* terminating the JVM instead of returning.
*/
@Tag("unit")
public class PerlExitExceptionTest {

@BeforeEach
void setUp() {
PerlLanguageProvider.resetAll();
}

@Test
void testExitZeroThrowsException() {
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "exit 0;";

PerlExitException thrown = assertThrows(PerlExitException.class, () -> {
PerlLanguageProvider.executePerlCode(options, true);
});

assertEquals(0, thrown.getExitCode(), "exit 0 should have exit code 0");
}

@Test
void testExitNonZeroThrowsException() {
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "exit 42;";

PerlExitException thrown = assertThrows(PerlExitException.class, () -> {
PerlLanguageProvider.executePerlCode(options, true);
});

assertEquals(42, thrown.getExitCode(), "exit 42 should have exit code 42");
}

@Test
void testExitAfterOutputThrowsException() {
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "print 'hello'; exit 0;";

PerlExitException thrown = assertThrows(PerlExitException.class, () -> {
PerlLanguageProvider.executePerlCode(options, true);
});

assertEquals(0, thrown.getExitCode());
}

@Test
void testScriptWithoutExitReturnsNormally() throws Exception {
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "my $x = 1 + 1;";

// Should not throw - script completes without calling exit()
assertDoesNotThrow(() -> {
PerlLanguageProvider.executePerlCode(options, true);
});
}

@Test
void testExceptionMessage() {
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "exit 123;";

PerlExitException thrown = assertThrows(PerlExitException.class, () -> {
PerlLanguageProvider.executePerlCode(options, true);
});

assertEquals("exit 123", thrown.getMessage());
}

@Test
void testExitInsideEvalNotCaught() {
// In Perl, exit() should NOT be caught by eval{} - it should always exit
CompilerOptions options = new CompilerOptions();
options.fileName = "<test>";
options.code = "eval { exit 99; }; print 'should not reach here';";

PerlExitException thrown = assertThrows(PerlExitException.class, () -> {
PerlLanguageProvider.executePerlCode(options, true);
});

assertEquals(99, thrown.getExitCode(), "exit inside eval should still throw PerlExitException");
}
}