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
4 changes: 4 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans.

## Work in progress

- CPAN/compiler tooling: add transitive prerequisites to the bundled-provider
manifest, provide a JAXP-backed `XML::LibXSLT`, and preserve descriptors for
anonymous handles stored in container lvalues. This unblocks
`Catmandu::CrossRef` and `AnyEvent::SMTP` without distribution preferences.
- Add Perl interpreter multiplicity and the supported ithread tranche across
the JVM and interpreter backends. Mutable execution state is owned by
independent `PerlRuntime` instances; child threads receive identity-aware
Expand Down
1 change: 1 addition & 0 deletions docs/reference/bundled-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ These are loaded automatically or via `use`:
|--------|---------------|-------|
| `XML::Parser` | Perl | |
| `XML::Parser::Expat` | Java | Uses Java SAX parser |
| `XML::LibXSLT` | Java + Perl | Core XSLT transformation API backed by JDK JAXP; requires bundled `XML::LibXML` |
| `HTML::Parser` | Java | |
| `HTML::Content::Extractor` | Java | Uses jsoup for HTML5 parsing; install the CPAN `.pm` with `jcpan` |

Expand Down
1 change: 1 addition & 0 deletions docs/reference/feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,7 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa
- ✅ **Text::CSV** module.
- ✅ **TOML** module.
- ✅ **XML::Parser** module backed by JDK SAX (replaces native libexpat XS).
- ✅ **XML::LibXSLT** core transformation API backed by JDK JAXP (replaces native libxslt XS).
- ✅ **YAML::PP** module.
- ✅ **YAML** module.
- ✅ **YAML::Syck** compatibility module backed by bundled `YAML::PP`.
Expand Down
23 changes: 16 additions & 7 deletions src/main/java/org/perlonjava/runtime/io/SocketIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ private void initializeInternetSocketStreams() {
}
try {
this.socket = socketChannel.socket();
if (socketChannel.isConnected()) {
// java.net.Socket streams cannot be obtained while their channel is
// non-blocking (IllegalBlockingModeException). Non-blocking sockets
// use SocketChannel reads/writes below and initialize streams only
// if they later transition back to blocking mode.
if (socketChannel.isConnected() && socketChannel.isBlocking()) {
this.inputStream = socket.getInputStream();
this.outputStream = socket.getOutputStream();
}
Expand Down Expand Up @@ -257,9 +261,7 @@ public RuntimeScalar connect(String address, int port) {
boolean finished = socketChannel.finishConnect();
if (finished) {
// Connection completed — return EISCONN to match POSIX behavior
this.socket = socketChannel.socket();
this.inputStream = socket.getInputStream();
this.outputStream = socket.getOutputStream();
initializeInternetSocketStreams();
getGlobalVariable("main::!").set(ErrnoVariable.EISCONN());
return scalarUndef;
}
Expand Down Expand Up @@ -307,9 +309,7 @@ public RuntimeScalar connect(String address, int port) {
return scalarUndef;
}
// Connected immediately
this.socket = socketChannel.socket();
this.inputStream = socket.getInputStream();
this.outputStream = socket.getOutputStream();
initializeInternetSocketStreams();
return scalarTrue;
}

Expand Down Expand Up @@ -1084,6 +1084,15 @@ public RuntimeScalar getsockname() {
*/
public RuntimeScalar getpeername() {
try {
// A non-blocking connect is only finalized in Java after
// finishConnect(). POSIX callers commonly wait for write readiness
// and then use getpeername() to distinguish success from failure
// (AnyEvent::Socket does exactly this), so complete the pending
// connection at this observation point just as read/write do.
if (!ensureConnected()) {
getGlobalVariable("main::!").set(ErrnoVariable.EAGAIN());
return scalarUndef;
}
if (socketChannel != null
&& socketChannel.getRemoteAddress() instanceof UnixDomainSocketAddress unixAddress) {
return packSockaddrUn(unixAddress);
Expand Down
37 changes: 20 additions & 17 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,8 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
RuntimeGlob anonGlob = new RuntimeGlob(null).setIO(oneFh);
newGlob.value = anonGlob;
RuntimeIO.registerGlobForFdRecycling(anonGlob, oneFh);
fileHandle.set(newGlob);
fileHandle.ioOwner = true;
RuntimeScalar assignedHandle = fileHandle.set(newGlob);
assignedHandle.ioOwner = true;
}
long pid = oneFh.getPid();
if (pid > 0) return new RuntimeScalar(pid);
Expand Down Expand Up @@ -896,12 +896,8 @@ else if (secondArg.type == RuntimeScalarType.GLOB || secondArg.type == RuntimeSc
// Register for GC-based fd recycling (mimics Perl's DESTROY on scope exit)
RuntimeIO.registerGlobForFdRecycling(anonGlob, fh);
// Use set() to modify the lvalue in place
fileHandle.set(newGlob);
// Mark this scalar as the IO owner so scopeExitCleanup will close
// the handle when the variable goes out of scope. Copies of this
// reference (via set()) won't have ioOwner=true, preventing
// premature close of shared handles (e.g., Test2's dup'd STDOUT).
fileHandle.ioOwner = true;
RuntimeScalar assignedHandle = fileHandle.set(newGlob);
assignedHandle.ioOwner = true;
}
long pid = fh.getPid();
if (pid > 0) return new RuntimeScalar(pid);
Expand Down Expand Up @@ -1953,13 +1949,8 @@ public static RuntimeScalar socket(int ctx, RuntimeBase... args) {
RuntimeGlob anonGlob = new RuntimeGlob(null).setIO(socketIO);
newGlob.value = anonGlob;
RuntimeIO.registerGlobForFdRecycling(anonGlob, socketIO);
socketHandle.set(newGlob);
// Mark this scalar as the IO owner so scopeExitCleanup will
// unregister the fd when the variable goes out of scope.
// Copies (via set()) won't have ioOwner=true, preventing
// premature fd unregistration when copies go out of scope
// (e.g., method argument copies in IO::Handle::fileno).
socketHandle.ioOwner = true;
RuntimeScalar assignedHandle = socketHandle.set(newGlob);
assignedHandle.ioOwner = true;
}
return scalarTrue;

Expand Down Expand Up @@ -2166,7 +2157,7 @@ public static RuntimeScalar listen(int ctx, RuntimeBase... args) {
/**
* accept(NEWSOCKET, GENERICSOCKET)
* Accepts a connection on a listening socket.
* Returns the packed sockaddr of the remote peer on success, false on failure.
* Returns the packed sockaddr of the remote peer on success, undef on failure.
*/
public static RuntimeScalar accept(int ctx, RuntimeBase... args) {
if (args.length < 2) {
Expand All @@ -2189,7 +2180,8 @@ public static RuntimeScalar accept(int ctx, RuntimeBase... args) {
// Accept the connection - returns a new SocketIO for the client
SocketIO clientSocketIO = listenSocketIO.acceptConnection();
if (clientSocketIO == null) {
return scalarFalse;
getGlobalVariable("main::!").set(ErrnoVariable.EAGAIN());
return scalarUndef;
}

// Wrap in RuntimeIO and associate with the NEWSOCKET glob
Expand Down Expand Up @@ -2424,6 +2416,17 @@ public static RuntimeScalar fcntl(int ctx, RuntimeBase... args) {
return scalarUndef;
}

IOHandle managedHandle = selectableHandle(fh.ioHandle);
if (managedHandle instanceof SocketIO socketIO) {
if (function == 3) { // F_GETFL
return new RuntimeScalar(socketIO.isBlocking() ? 0 : 2048);
}
if (function == 4) { // F_SETFL
socketIO.setBlocking((arg & 2048) == 0);
return scalarTrue;
}
}

// Get the file descriptor number
RuntimeScalar filenoResult = fh.ioHandle.fileno();
int fd = filenoResult.getDefinedBoolean() ? filenoResult.getInt() : -1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ private static void setupISA() {
// Node wrapping helpers
// ================================================================

static RuntimeScalar wrapNode(Node node) {
public static RuntimeScalar wrapNode(Node node) {
if (node == null) return scalarUndef;
RuntimeHash hash = new RuntimeHash();
hash.put(NODE_KEY, new RuntimeScalar(node));
Expand All @@ -485,7 +485,7 @@ private static ReaderState getReader(RuntimeScalar self) {
throw new RuntimeException("Not a valid XML::LibXML::Reader object");
}

static Node getNode(RuntimeScalar self) {
public static Node getNode(RuntimeScalar self) {
if (self == null || self.type == RuntimeScalarType.UNDEF) return null;
RuntimeHash hash;
try { hash = self.hashDerefRaw(); }
Expand Down
181 changes: 181 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/XMLLibXSLT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.perlonjava.runtime.perlmodule;

import org.perlonjava.runtime.operators.ReferenceOperators;
import org.perlonjava.runtime.runtimetypes.*;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;
import java.io.StringWriter;

/** Java XS provider for the core XML::LibXSLT transformation surface. */
public class XMLLibXSLT extends PerlModuleBase {
public static final String XS_VERSION = "2.003000";
private static final String STATE_KEY = "_xslt_state";
private static final String RESULT_KEY = "_xslt_result";

private record StylesheetState(Templates templates) {}
private record TextResult(String value) {}

public XMLLibXSLT() {
super("XML::LibXSLT", false);
}

public static void initialize() {
XMLLibXSLT module = new XMLLibXSLT();
try {
module.registerMethod("INIT_THREAD_SUPPORT", "noop", null);
module.registerMethod("HAVE_EXSLT", "falseValue", null);
module.registerMethod("LIBXSLT_DOTTED_VERSION", "dottedVersion", null);
module.registerMethod("LIBXSLT_VERSION", "numericVersion", null);
module.registerMethod("LIBXSLT_RUNTIME_VERSION", "numericVersion", null);
module.registerMethod("_parse_stylesheet", "parseStylesheet", null);
module.registerMethod("_parse_stylesheet_file", "parseStylesheetFile", null);
module.registerMethodInPackage("XML::LibXSLT::Stylesheet", "transform", "transform");
module.registerMethodInPackage("XML::LibXSLT::Stylesheet", "_output_string", "outputString");
module.registerMethodInPackage("XML::LibXSLT::Stylesheet", "output_method", "outputMethod");
module.registerMethodInPackage("XML::LibXSLT::Stylesheet", "output_encoding", "outputEncoding");
module.registerMethodInPackage("XML::LibXSLT::Stylesheet", "media_type", "mediaType");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

private static RuntimeScalar blessedState(String key, Object state, String packageName) {
RuntimeHash hash = new RuntimeHash();
hash.put(key, new RuntimeScalar(state));
return ReferenceOperators.bless(
hash.createReferenceWithTrackedElements(), new RuntimeScalar(packageName));
}

private static Object state(RuntimeScalar self, String key) {
RuntimeScalar value = self.hashDerefRaw().get(key);
if (value == null || value.value == null) {
throw new RuntimeException("Invalid XML::LibXSLT object");
}
return value.value;
}

public static RuntimeList noop(RuntimeArray args, int ctx) {
return new RuntimeScalar(1).getList();
}

public static RuntimeList falseValue(RuntimeArray args, int ctx) {
return new RuntimeScalar(0).getList();
}

public static RuntimeList dottedVersion(RuntimeArray args, int ctx) {
return new RuntimeScalar("JAXP").getList();
}

public static RuntimeList numericVersion(RuntimeArray args, int ctx) {
return new RuntimeScalar(0).getList();
}

public static RuntimeList parseStylesheet(RuntimeArray args, int ctx) {
try {
Node node = XMLLibXML.getNode(args.get(1).scalar());
Templates templates = TransformerFactory.newInstance().newTemplates(new DOMSource(node));
return blessedState(STATE_KEY, new StylesheetState(templates),
"XML::LibXSLT::Stylesheet").getList();
} catch (Exception e) {
throw new RuntimeException("XML::LibXSLT stylesheet parse failed: " + e.getMessage(), e);
}
}

public static RuntimeList parseStylesheetFile(RuntimeArray args, int ctx) {
try {
Templates templates = TransformerFactory.newInstance()
.newTemplates(new StreamSource(args.get(1).toString()));
return blessedState(STATE_KEY, new StylesheetState(templates),
"XML::LibXSLT::Stylesheet").getList();
} catch (Exception e) {
throw new RuntimeException("XML::LibXSLT stylesheet parse failed: " + e.getMessage(), e);
}
}

private static Transformer transformer(RuntimeArray args) throws Exception {
StylesheetState stylesheet = (StylesheetState) state(args.get(0).scalar(), STATE_KEY);
Transformer transformer = stylesheet.templates().newTransformer();
for (int i = 2; i + 1 < args.size(); i += 2) {
String value = args.get(i + 1).toString();
if (value.length() >= 2 && value.startsWith("'") && value.endsWith("'")) {
value = value.substring(1, value.length() - 1);
}
transformer.setParameter(args.get(i).toString(), value);
}
return transformer;
}

public static RuntimeList transform(RuntimeArray args, int ctx) {
try {
Transformer transformer = transformer(args);
Node input = XMLLibXML.getNode(args.get(1).scalar());
String method = transformer.getOutputProperty(OutputKeys.METHOD);
if ("text".equalsIgnoreCase(method)) {
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(input), new StreamResult(writer));
return blessedState(RESULT_KEY, new TextResult(writer.toString()),
"XML::LibXSLT::Result").getList();
}
DOMResult result = new DOMResult();
transformer.transform(new DOMSource(input), result);
Node output = result.getNode();
if (output instanceof Document document) return XMLLibXML.wrapNode(document).getList();
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
document.appendChild(document.importNode(output, true));
return XMLLibXML.wrapNode(document).getList();
} catch (Exception e) {
throw new RuntimeException("XML::LibXSLT transform failed: " + e.getMessage(), e);
}
}

public static RuntimeList outputString(RuntimeArray args, int ctx) {
try {
RuntimeScalar result = args.get(1).scalar();
try {
TextResult text = (TextResult) state(result, RESULT_KEY);
return new RuntimeScalar(text.value()).getList();
} catch (Exception ignored) {
StringWriter writer = new StringWriter();
StylesheetState stylesheet = (StylesheetState) state(args.get(0).scalar(), STATE_KEY);
Transformer serializer = stylesheet.templates().newTransformer();
serializer.transform(new DOMSource(XMLLibXML.getNode(result)), new StreamResult(writer));
return new RuntimeScalar(writer.toString()).getList();
}
} catch (Exception e) {
throw new RuntimeException("XML::LibXSLT output failed: " + e.getMessage(), e);
}
}

private static String outputProperty(RuntimeScalar self, String name) {
try {
StylesheetState stylesheet = (StylesheetState) state(self, STATE_KEY);
String value = stylesheet.templates().getOutputProperties().getProperty(name);
return value == null ? "" : value;
} catch (Exception e) {
return "";
}
}

public static RuntimeList outputMethod(RuntimeArray args, int ctx) {
return new RuntimeScalar(outputProperty(args.get(0).scalar(), OutputKeys.METHOD)).getList();
}

public static RuntimeList outputEncoding(RuntimeArray args, int ctx) {
return new RuntimeScalar(outputProperty(args.get(0).scalar(), OutputKeys.ENCODING)).getList();
}

public static RuntimeList mediaType(RuntimeArray args, int ctx) {
return new RuntimeScalar(outputProperty(args.get(0).scalar(), OutputKeys.MEDIA_TYPE)).getList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ public static void callDestroy(RuntimeBase referent) {
// %DEFERRED hash), causing infinite recursion in Moo/DBIx::Class.
if (referent instanceof RuntimeCode code) {
if (code.stashRefCount <= 0) {
if (ReachabilityWalker.strongCycleRetainsWeakReferent(code)) {
code.refCount = 1;
return;
}
code.releaseCaptures();
}
}
Expand Down
Loading
Loading