Skip to content

Fix local *FH pattern for independent filehandles and add fileno support#385

Merged
fglock merged 9 commits into
masterfrom
fix/multiple-filehandle-copies
Mar 27, 2026
Merged

Fix local *FH pattern for independent filehandles and add fileno support#385
fglock merged 9 commits into
masterfrom
fix/multiple-filehandle-copies

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Mar 27, 2026

Summary

The do { local *FH; open FH, ...; *FH } pattern (used by Log::Log4perl) was creating filehandles that shared the same IO slot instead of being independent.

Root Cause

In EmitOperatorLocal.java, when processing local *FH, the code was calling createDetachedCopy() before dynamicSaveState(), so local operated on a copy instead of the actual global glob from globalIORefs. Later *FH accesses returned the original glob (unmodified by local).

Fix

  • EmitOperatorLocal.java: local *GLOB now directly emits getGlobalIO() WITHOUT createDetachedCopy(), ensuring dynamicSaveState() operates on the actual global glob
  • CustomFileChannel.java: Added synthetic fileno support since Java doesn't expose OS file descriptors (each handle gets a unique fd starting from 3)

Test Plan

  • Unit test local_glob_filehandle.t passes (tests independent filehandles, Log::Log4perl pattern)
  • make passes all tests

Generated with Devin

fglock and others added 7 commits March 27, 2026 15:46
When using `do { local *FH; *FH }` to create multiple filehandle copies,
each copy now gets its own independent IO slot. Previously, all copies
shared the same IO reference, causing files opened on one copy to
overwrite files opened on another copy.

Changes:
- RuntimeGlob.createDetachedCopy(): Create a new IO RuntimeScalar instead
  of sharing the reference
- EmitVariable.java: Call createDetachedCopy() when returning globs
- SlowOpcodeHandler.java: Same change for interpreter backend
- IOOperator.java: Update global glob when opening named filehandles

Fixes Log::Log4perl t/026FileApp.t test failure.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
When doing *FH = *TESTFILE, the set(RuntimeGlob) method was only
updating the detached copy IO slot, not the global glob. This caused
<FH> to fail because the global FH IO was still empty.

The fix updates both the detached copy AND the global glob IO slot.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
When calling opendir(DIR, path), the glob DIR is a detached copy.
The fix ensures the global glob's IO is also updated, matching the
fix already applied to open() in IOOperator.java.

This fixes regressions in op/stat_errors.t and other directory tests.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The previous commit incorrectly called createDetachedCopy() at glob load time,
which caused each access to `*FH` to return a different glob object. This broke:
- `tie *FH, ...` followed by `tied *FH` (returned undef)
- Multiple references to the same glob via `\*FH` (different objects)

The fix is to only create detached copies when ASSIGNING a glob to a scalar
(via RuntimeScalar(RuntimeGlob) constructor), not at load time. This way:
- `\*FH` returns the same glob reference each time (correct for tie/tied)
- `my $fh = *FH` creates a detached copy (correct for Log::Log4perl)

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The independent IO slots caused a regression in uni/gv.t test 188
(PVLV: sv_2io stringifieth not). This test requires:

  $_ = *quin;
  open *quin, test.pl;
  # $_ must see the IO opened through *quin

With independent IO slots, $_ had its own IO that was not updated when
opening through *quin.

Reverting to shared IO behavior:
- createDetachedCopy() shares the IO reference (copy.IO = this.IO)
- Removed redundant global glob updates from IOOperator and Directory

The Log::Log4perl do { local *FH; *FH } pattern issue remains a
pre-existing bug on master that needs a different fix approach.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The do { local *FH; open FH, ...; *FH } pattern (used by Log::Log4perl)
was creating filehandles that shared the same IO slot instead of being
independent.

Root cause: EmitOperatorLocal was calling createDetachedCopy() before
dynamicSaveState(), so local operated on a copy instead of the actual
global glob from globalIORefs.

Fix: In EmitOperatorLocal, local *GLOB now directly emits getGlobalIO()
WITHOUT createDetachedCopy(), ensuring dynamicSaveState() operates on
the actual global glob.

Also added synthetic fileno support to CustomFileChannel since Java
does not expose OS file descriptors.

Generated with Devin: https://cli.devin.ai/docs

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The glob assignment *FH = *TESTFILE was not properly updating the global
glob's IO slot, only updating the detached copy. Now we update both.

Also added fd recycling for synthetic file descriptors - when a
CustomFileChannel is closed, its fd is returned to a pool for reuse.
This makes fileno() return more stable values.

Test results:
- base/rs.t: Now passes 41/41 (was 21/41) - FIXED
- run/switchF1.t: Now passes 5/5 - FIXED
- local_glob_filehandle.t: Passes 5/5 - core bug fix verified
- io/perlio_leaks.t: Fails due to JVM GC (no scope-based fd cleanup)
- io/dup.t: Pre-existing failures (same on master)
- comp/proto.t: Pre-existing failures (same on master)

Generated with Devin: https://cli.devin.ai/docs

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock force-pushed the fix/multiple-filehandle-copies branch from 94be062 to 280f92c Compare March 27, 2026 14:47
fglock and others added 2 commits March 27, 2026 16:08
Add hashCode() and equals() overrides to RuntimeGlob based on globName.
This ensures that detached copies (used by local *FH) compare equal to
the original glob, fixing the regression in comp/proto.t where
`\*FOO eq \*FOO` was returning false.

- hashCode() returns globName.hashCode() so all copies stringify the same
- equals() compares globName so \*FOO eq \*FOO works correctly
- Revert unnecessary fileno changes (not needed for the core fix)
- Simplify test file

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
getDoubleRef() now returns Integer.toUnsignedLong(hashCode()) to match
what hex() returns when parsing the stringified address. This fixes
op/bless.t and uni/bless.t tests that compare hex(address) == object.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock merged commit ff175e3 into master Mar 27, 2026
2 checks passed
@fglock fglock deleted the fix/multiple-filehandle-copies branch March 27, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant