diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 048529db4..e4a4198a9 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -18,10 +18,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 21 + - name: Set up JDK 22 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '22' distribution: 'temurin' - name: Setup Gradle diff --git a/Dockerfile b/Dockerfile index 7066c00eb..c9cf0b0d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # For instructions on building and running this Docker image, # please refer to docs/DOCKER.md -# Use Eclipse Temurin JDK 21 as the base image -FROM eclipse-temurin:21-jdk AS build +# Use Eclipse Temurin JDK 22 as the base image +FROM eclipse-temurin:22-jdk AS build # Install Maven RUN apt-get update && \ @@ -19,10 +19,10 @@ COPY . . RUN mvn clean package # Use Eclipse Temurin JDK image to run the application -FROM eclipse-temurin:21-jdk +FROM eclipse-temurin:22-jdk # Copy the built JAR file from the Maven container -COPY --from=build /app/target/perlonjava-3.0.0.jar /app/perlonjava.jar +COPY --from=build /app/target/perlonjava-5.42.0.jar /app/perlonjava.jar # Set the entry point to run the JAR file ENTRYPOINT ["java", "-jar", "/app/perlonjava.jar"] diff --git a/QUICKSTART.md b/QUICKSTART.md index 4146ebbb7..e5a1e96d9 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -4,7 +4,7 @@ Get PerlOnJava running in 5 minutes. ## Prerequisites -- **Java Development Kit (JDK) 21 or later** +- **Java Development Kit (JDK) 22 or later** - **Git** for cloning the repository - **Make** (optional - can use Gradle directly) @@ -14,7 +14,7 @@ Check your Java version: ```bash java -version ``` -Should show version 21 or higher. +Should show version 22 or higher. **Important:** Check you have the **JDK** (not just JRE): ```bash @@ -26,8 +26,8 @@ Should show the same version. If `javac: command not found`, you need to install - Use your system's package manager, or - Download from a JDK provider (Adoptium, Oracle, Azul, Amazon Corretto, etc.) - Common package manager commands: - - **macOS**: `brew install openjdk@21` - - **Ubuntu/Debian**: `sudo apt install openjdk-21-jdk` + - **macOS**: `brew install openjdk@22` + - **Ubuntu/Debian**: `sudo apt install openjdk-22-jdk` - **Windows**: Use package manager like [Chocolatey](https://chocolatey.org/) or [Scoop](https://scoop.sh/) ## Installation diff --git a/build.gradle b/build.gradle index 19f6e083a..fa7e524dc 100644 --- a/build.gradle +++ b/build.gradle @@ -121,7 +121,7 @@ ospackage { packageName = 'perlonjava' version = project.version maintainer = 'Flavio Soibelmann Glock ' - // Java 21+ is required at runtime (any distribution: Oracle, Azul, Temurin, OpenJDK, etc.) + // Java 22+ is required at runtime (any distribution: Oracle, Azul, Temurin, OpenJDK, etc.) into '/opt/perlonjava' from('build/install/perlonjava') { @@ -137,10 +137,10 @@ ospackage { link('/usr/local/bin/jperl', '/opt/perlonjava/bin/jperl') } -// Java toolchain configuration - requires Java 21 +// Java toolchain configuration - requires Java 22 (for FFM API) java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(22) } } @@ -159,7 +159,7 @@ dependencies { implementation libs.snakeyaml.engine // YAML processing implementation libs.tomlj // TOML processing implementation libs.commons.csv // CSV processing - implementation libs.jnr.posix // Native access + // JNR-POSIX removed - using Java FFM API for native access (Java 22+) // Testing dependencies testImplementation libs.junit.jupiter.api diff --git a/dev/design/ffm_migration.md b/dev/design/ffm_migration.md new file mode 100644 index 000000000..1c0a31f87 --- /dev/null +++ b/dev/design/ffm_migration.md @@ -0,0 +1,530 @@ +# FFM Migration: Replace JNR-POSIX with Java Foreign Function & Memory API + +## Overview + +This document outlines the plan to migrate PerlOnJava from JNR-POSIX to Java's Foreign Function & Memory (FFM) API (JEP 454), eliminating the `sun.misc.Unsafe` warnings that appear on Java 24+. + +### Problem Statement + +JNR-POSIX depends on JFFI, which uses `sun.misc.Unsafe` for native memory access. Starting with Java 24, warnings are issued by default: + +``` +WARNING: A terminally deprecated method in sun.misc.Unsafe has been called +WARNING: sun.misc.Unsafe::putLong has been called by com.kenai.jffi.UnsafeMemoryIO$UnsafeMemoryIO64 +``` + +The `sun.misc.Unsafe` memory-access methods are scheduled for removal in a future JDK release (JEP 471/498). + +### Solution + +Migrate to Java's FFM API (finalized in Java 22), which provides a safe, supported replacement for native function calls. + +### Requirements + +- **Minimum Java version**: 22 (FFM finalized) +- **Backwards compatibility**: Maintain all existing Perl functionality +- **Windows support**: Provide Windows-specific implementations where POSIX is unavailable + +## Current JNR-POSIX Usage Inventory + +### Functions Used + +| Function | Location | Windows Alternative | +|----------|----------|---------------------| +| `chmod(path, mode)` | `Operator.java` | `Files.setPosixFilePermissions()` or ACL | +| `kill(pid, signal)` | `KillOperator.java` | `ProcessHandle.destroy()` / `destroyForcibly()` | +| `errno()` | Multiple | Thread-local errno storage | +| `isatty(fd)` | `FileTestOperator.java`, `DebugHooks.java` | `System.console() != null` | +| `strerror(errno)` | `POSIX.java` | Java error message mapping | +| `fcntl(fd, cmd, arg)` | `IOOperator.java` | Limited support via NIO | +| `stat(path)` / `lstat(path)` | `Stat.java` | `Files.readAttributes()` | +| `link(old, new)` | `NativeUtils.java` | `Files.createLink()` | +| `getppid()` | `NativeUtils.java` | `ProcessHandle.current().parent()` | +| `getuid()` / `geteuid()` | `NativeUtils.java` | Hash-based simulation | +| `getgid()` / `getegid()` | `NativeUtils.java` | Hash-based simulation | +| `getpwnam(name)` | `ExtendedNativeUtils.java` | User property simulation | +| `getpwuid(uid)` | `ExtendedNativeUtils.java` | User property simulation | +| `getpwent()` / `setpwent()` / `endpwent()` | `ExtendedNativeUtils.java` | `/etc/passwd` parsing or simulation | +| `umask(mask)` | `UmaskOperator.java` | Simulation with default values | +| `utimes(path, atime, mtime)` | `UtimeOperator.java` | `Files.setLastModifiedTime()` + custom | +| `waitpid(pid, status, flags)` | `WaitpidOperator.java` | `Process.waitFor()` | + +### Data Structures Used + +- `FileStat` - stat structure (dev, ino, mode, nlink, uid, gid, rdev, size, times, blocks) +- `Passwd` - password entry (name, passwd, uid, gid, gecos, home, shell) + +## FFM API Overview + +### Key Classes (Java 22+) + +```java +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +``` + +### Basic Pattern + +```java +// 1. Get the native linker and symbol lookup +Linker linker = Linker.nativeLinker(); +SymbolLookup stdlib = linker.defaultLookup(); + +// 2. Find the native function +MemorySegment killSymbol = stdlib.find("kill").orElseThrow(); + +// 3. Create a method handle with the function descriptor +MethodHandle kill = linker.downcallHandle( + killSymbol, + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) +); + +// 4. Call the function +int result = (int) kill.invokeExact(pid, signal); +``` + +### Memory Management + +```java +try (Arena arena = Arena.ofConfined()) { + // Allocate native memory for structs + MemorySegment statBuf = arena.allocate(STAT_LAYOUT); + + // Call native function + int result = (int) statHandle.invokeExact(pathSegment, statBuf); + + // Read struct fields + long size = statBuf.get(ValueLayout.JAVA_LONG, ST_SIZE_OFFSET); +} +// Memory automatically freed when arena closes +``` + +## Architecture + +### New Package Structure + +``` +src/main/java/org/perlonjava/runtime/nativ/ +├── PosixLibrary.java # Current JNR-POSIX wrapper (to be replaced) +├── NativeUtils.java # Existing utility class +├── ExtendedNativeUtils.java # Existing utility class +└── ffm/ # New FFM-based implementations + ├── FFMPosix.java # Main FFM POSIX interface + ├── FFMPosixLinux.java # Linux-specific implementations + ├── FFMPosixMacOS.java # macOS-specific implementations + ├── FFMPosixWindows.java # Windows-specific implementations + ├── StatStruct.java # stat structure wrapper + ├── PasswdStruct.java # passwd structure wrapper + └── ErrnoHandler.java # errno management +``` + +### Platform Detection + +```java +public class FFMPosix { + private static final FFMPosixInterface INSTANCE = createInstance(); + + private static FFMPosixInterface createInstance() { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + return new FFMPosixWindows(); + } else if (os.contains("mac")) { + return new FFMPosixMacOS(); + } else { + return new FFMPosixLinux(); // Default for Linux/Unix + } + } + + public static FFMPosixInterface get() { + return INSTANCE; + } +} +``` + +## Windows Compatibility Strategy + +Windows doesn't have POSIX APIs. For each function, we need a Windows-specific approach: + +### Already Implemented (Pure Java) + +These already have Windows fallbacks in the codebase: + +| Function | Windows Implementation | +|----------|----------------------| +| `getppid()` | `ProcessHandle.current().parent().pid()` | +| `getuid()` / `geteuid()` | Hash of `user.name` property | +| `getgid()` / `getegid()` | Hash of `COMPUTERNAME` env var | +| `link()` | `Files.createLink()` | +| `kill()` | `ProcessHandle.destroy()` / `destroyForcibly()` | + +### Requires Windows API Calls + +| POSIX Function | Windows API | Notes | +|----------------|-------------|-------| +| `isatty()` | `GetConsoleMode()` | Check if handle is console | +| `chmod()` | `SetFileAttributes()` | Limited (read-only flag only) | +| `stat()` | `GetFileAttributesEx()` | Different struct layout | +| `umask()` | N/A | Simulate with thread-local default | +| `fcntl()` | Various | Partial support only | + +### Windows FFM Example: isatty() + +```java +public class FFMPosixWindows implements FFMPosixInterface { + private static final MethodHandle GetStdHandle; + private static final MethodHandle GetConsoleMode; + + static { + Linker linker = Linker.nativeLinker(); + SymbolLookup kernel32 = SymbolLookup.libraryLookup("kernel32", Arena.global()); + + GetStdHandle = linker.downcallHandle( + kernel32.find("GetStdHandle").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT) + ); + + GetConsoleMode = linker.downcallHandle( + kernel32.find("GetConsoleMode").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + } + + private static final int STD_INPUT_HANDLE = -10; + private static final int STD_OUTPUT_HANDLE = -11; + private static final int STD_ERROR_HANDLE = -12; + + @Override + public boolean isatty(int fd) { + try (Arena arena = Arena.ofConfined()) { + int stdHandle = switch (fd) { + case 0 -> STD_INPUT_HANDLE; + case 1 -> STD_OUTPUT_HANDLE; + case 2 -> STD_ERROR_HANDLE; + default -> throw new IllegalArgumentException("Invalid fd: " + fd); + }; + + MemorySegment handle = (MemorySegment) GetStdHandle.invokeExact(stdHandle); + MemorySegment mode = arena.allocate(ValueLayout.JAVA_INT); + int result = (int) GetConsoleMode.invokeExact(handle, mode); + return result != 0; + } catch (Throwable e) { + return false; + } + } +} +``` + +## Implementation Phases + +### Phase 1: Infrastructure (Week 1) + +**Goal**: Set up FFM framework without changing existing behavior + +1. Create `ffm/` package structure +2. Implement `FFMPosixInterface` with all method signatures +3. Create platform-specific stub implementations +4. Add feature flag to switch between JNR-POSIX and FFM: + ```java + // System property to enable FFM (default: false initially) + boolean useFFM = Boolean.getBoolean("perlonjava.ffm.enabled"); + ``` + +**Files to create**: +- `FFMPosixInterface.java` +- `FFMPosix.java` (factory) +- `FFMPosixLinux.java` (stubs) +- `FFMPosixMacOS.java` (stubs) +- `FFMPosixWindows.java` (stubs) + +### Phase 2: Simple Functions (Week 2) + +**Goal**: Implement functions that don't require complex structs + +Implement in order of complexity: +1. `kill(pid, signal)` - Linux/macOS only +2. `isatty(fd)` - All platforms +3. `errno()` / `strerror()` - All platforms +4. `getuid()`, `geteuid()`, `getgid()`, `getegid()` - Linux/macOS +5. `getppid()` - Linux/macOS +6. `umask(mask)` - Linux/macOS +7. `chmod(path, mode)` - Linux/macOS + +### Phase 3: Struct-Based Functions (Week 3) + +**Goal**: Implement functions requiring native struct handling + +1. Define struct layouts: + ```java + // Linux stat struct (x86_64) + public static final MemoryLayout STAT_LAYOUT = MemoryLayout.structLayout( + ValueLayout.JAVA_LONG.withName("st_dev"), + ValueLayout.JAVA_LONG.withName("st_ino"), + ValueLayout.JAVA_LONG.withName("st_nlink"), + ValueLayout.JAVA_INT.withName("st_mode"), + ValueLayout.JAVA_INT.withName("st_uid"), + // ... etc + ); + ``` + +2. Implement: + - `stat(path)` / `lstat(path)` + - `getpwnam(name)` / `getpwuid(uid)` + - `getpwent()` / `setpwent()` / `endpwent()` + - `utimes(path, times)` + - `fcntl(fd, cmd, arg)` + - `waitpid(pid, status, flags)` + +### Phase 4: Windows Support (Week 4) + +**Goal**: Complete Windows implementations + +1. `isatty()` via `GetConsoleMode()` +2. `stat()` via `GetFileAttributesEx()` or existing Java NIO +3. Verify all existing Java fallbacks work correctly + +### Phase 5: Testing & Migration (Week 5) + +**Goal**: Enable FFM by default, update minimum Java version + +1. **Update minimum Java version to 22**: + - Update `build.gradle` sourceCompatibility/targetCompatibility + - Update `pom.xml` maven.compiler.source/target + - Update CI workflows (`.github/workflows/`) + - Update documentation (README.md, QUICKSTART.md, installation.md) + - Update presentations +2. Run full test suite with `perlonjava.ffm.enabled=true` +3. Fix any discrepancies +4. Change default to FFM (flip feature flag) +5. Mark JNR-POSIX code as deprecated +6. Remove `--sun-misc-unsafe-memory-access` flag logic from jperl/jperl.bat + +### Phase 6: Cleanup (Week 6) + +**Goal**: Remove JNR-POSIX dependency + +1. Remove JNR-POSIX from dependencies +2. Delete deprecated code +3. Update documentation + +## Struct Layouts Reference + +### Linux x86_64 stat + +```java +// sizeof(struct stat) = 144 bytes on Linux x86_64 +public static final StructLayout STAT_LAYOUT_LINUX_X64 = MemoryLayout.structLayout( + ValueLayout.JAVA_LONG.withName("st_dev"), // offset 0 + ValueLayout.JAVA_LONG.withName("st_ino"), // offset 8 + ValueLayout.JAVA_LONG.withName("st_nlink"), // offset 16 + ValueLayout.JAVA_INT.withName("st_mode"), // offset 24 + ValueLayout.JAVA_INT.withName("st_uid"), // offset 28 + ValueLayout.JAVA_INT.withName("st_gid"), // offset 32 + MemoryLayout.paddingLayout(4), // padding + ValueLayout.JAVA_LONG.withName("st_rdev"), // offset 40 + ValueLayout.JAVA_LONG.withName("st_size"), // offset 48 + ValueLayout.JAVA_LONG.withName("st_blksize"), // offset 56 + ValueLayout.JAVA_LONG.withName("st_blocks"), // offset 64 + ValueLayout.JAVA_LONG.withName("st_atime"), // offset 72 + ValueLayout.JAVA_LONG.withName("st_atime_ns"), // offset 80 + ValueLayout.JAVA_LONG.withName("st_mtime"), // offset 88 + ValueLayout.JAVA_LONG.withName("st_mtime_ns"), // offset 96 + ValueLayout.JAVA_LONG.withName("st_ctime"), // offset 104 + ValueLayout.JAVA_LONG.withName("st_ctime_ns"), // offset 112 + MemoryLayout.sequenceLayout(3, ValueLayout.JAVA_LONG) // reserved +); +``` + +### macOS stat + +```java +// macOS uses different struct layout +public static final StructLayout STAT_LAYOUT_MACOS = MemoryLayout.structLayout( + ValueLayout.JAVA_INT.withName("st_dev"), + ValueLayout.JAVA_SHORT.withName("st_mode"), + ValueLayout.JAVA_SHORT.withName("st_nlink"), + ValueLayout.JAVA_LONG.withName("st_ino"), + ValueLayout.JAVA_INT.withName("st_uid"), + ValueLayout.JAVA_INT.withName("st_gid"), + ValueLayout.JAVA_INT.withName("st_rdev"), + // ... timespec structs for atime, mtime, ctime + ValueLayout.JAVA_LONG.withName("st_size"), + ValueLayout.JAVA_LONG.withName("st_blocks"), + ValueLayout.JAVA_INT.withName("st_blksize"), + // ... flags, gen, etc +); +``` + +## Error Handling + +### errno Management + +FFM doesn't automatically capture `errno`. We need to use the `Linker.Option.captureCallState()` option: + +```java +MethodHandle kill = linker.downcallHandle( + killSymbol, + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), + Linker.Option.captureCallState("errno") +); + +try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) kill.invokeExact(capturedState, pid, signal); + if (result == -1) { + int errno = capturedState.get(ValueLayout.JAVA_INT, 0); + // Handle error + } +} +``` + +## Testing Strategy + +### Unit Tests + +Create tests for each FFM function comparing output with system Perl: + +```java +@Test +void testStat() { + // Create test file + Path testFile = Files.createTempFile("test", ".txt"); + Files.writeString(testFile, "content"); + + // Get stat via FFM + StatResult ffmStat = FFMPosix.get().stat(testFile.toString()); + + // Verify against Java NIO + BasicFileAttributes attrs = Files.readAttributes(testFile, BasicFileAttributes.class); + assertEquals(attrs.size(), ffmStat.size()); + + // Verify against Perl + String perlResult = runPerl("my @s = stat('" + testFile + "'); print $s[7]"); + assertEquals(perlResult, String.valueOf(ffmStat.size())); +} +``` + +### Integration Tests + +Run existing Perl test suite: +```bash +PERLONJAVA_FFM_ENABLED=true perl dev/tools/perl_test_runner.pl perl5_t/t +``` + +### Platform-Specific CI + +Ensure CI runs on: +- Linux x86_64 (Ubuntu) +- macOS arm64 (Apple Silicon) +- macOS x86_64 (Intel) +- Windows x64 + +## Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| Struct layout varies by platform/arch | Create platform-specific layouts, detect at runtime | +| FFM API changes in future Java | FFM is finalized in Java 22, API is stable | +| Performance regression | Benchmark critical paths, FFM should be similar to JNR | +| Windows functionality gaps | Accept limitations, document differences | +| Breaking existing behavior | Feature flag allows gradual rollout | + +## Dependencies + +### Remove +```groovy +// build.gradle - REMOVE after migration +implementation libs.jnr.posix +``` + +### Add (none - FFM is part of JDK) + +No new dependencies required. FFM is part of the Java standard library since Java 22. + +## Documentation Updates + +1. Update `README.md` with Java 22+ requirement +2. Update `QUICKSTART.md` with Java 22+ requirement +3. Update `docs/getting-started/installation.md` with Java 22+ requirement +4. Update `dev/presentations/` slides with Java 22+ requirement +5. Remove `--sun-misc-unsafe-memory-access` documentation from jperl scripts +6. Document any Windows-specific limitations + +### Files to Update for Java 22 + +| File | Change | +|------|--------| +| `build.gradle` | `sourceCompatibility = JavaVersion.VERSION_22` | +| `pom.xml` | `22` | +| `.github/workflows/*.yml` | `java-version: '22'` | +| `README.md` | "Requires Java 22+" | +| `QUICKSTART.md` | "JDK 22 or later" | +| `docs/getting-started/installation.md` | "Java 22 or higher" | +| `dev/presentations/**/*.md` | "Requires: Java 22+" | + +## Progress Tracking + +### Current Status: Phase 6 complete - JNR-POSIX dependency removed + +### Completed Phases +- [x] Phase 1: Infrastructure (2026-03-26) + - Created FFMPosixInterface with all method signatures + - Created FFMPosix factory with platform detection + - Created stub implementations for Linux, macOS, Windows + - Added feature flag (perlonjava.ffm.enabled) + - Windows implementation includes Java/ProcessHandle fallbacks +- [x] Phase 2: Simple Functions (2026-03-26) + - Implemented FFM-based getuid, geteuid, getgid, getegid, getppid, umask, chmod, kill, isatty for Linux/macOS + - Added $< (REAL_UID) and $> (EFFECTIVE_UID) special variables with lazy evaluation + - Fixed getppid JVM bytecode emission (was missing explicit handler in EmitOperatorNode) + - Updated Java version requirement to 22 (FFM finalized in Java 22) + - Files: FFMPosixLinux.java, ScalarSpecialVariable.java, GlobalContext.java, EmitOperatorNode.java, EmitOperator.java, build.gradle +- [x] Phase 3: Struct-Based Functions (2026-03-26) + - Implemented stat() and lstat() with FFM + - Added platform-specific struct stat layouts for Linux x86_64 and macOS x86_64/arm64 + - Implemented getpwnam, getpwuid, getpwent, setpwent, endpwent with FFM + - Added platform-specific struct passwd layouts for Linux and macOS + - Helper functions: readStatResult(), readPasswdEntry(), readCString() + - Proper errno capture using Linker.Option.captureCallState() + - Tested: stat values match native Perl exactly +- [x] Phase 4: Windows Support (2026-03-26) + - Windows uses Java fallbacks (ProcessHandle, NIO) - no native FFM calls needed + - isatty via System.console() check + - stat via Files.readAttributes() +- [x] Phase 5: Testing & Migration (2026-03-26) + - FFM enabled by default (perlonjava.ffm.enabled=true) + - All 122 unit tests pass + - Updated Java minimum version to 22 in build.gradle + - All JNR-POSIX code replaced with FFM equivalents + - Migrated files: NativeUtils, Stat, KillOperator, Operator (chmod), FileTestOperator (isatty), + DebugHooks (isatty), UmaskOperator, POSIX (strerror), IOOperator (fcntl), UtimeOperator (utimes), + WaitpidOperator, ExtendedNativeUtils (passwd functions) +- [x] Phase 6: Cleanup (2026-03-26) + - Removed JNR-POSIX from build.gradle dependencies + - Removed JNR-POSIX from libs.versions.toml + - Simplified PosixLibrary.java (removed JNR-POSIX references) + - Removed `--sun-misc-unsafe-memory-access` flag from jperl and jperl.bat scripts + - Updated documentation to remove sun.misc.Unsafe flag references + +### Migration Complete + +The FFM migration is complete. PerlOnJava no longer depends on JNR-POSIX and uses Java's +Foreign Function & Memory API (FFM) for all native system calls. This eliminates the +sun.misc.Unsafe deprecation warnings that appeared on Java 24+. + +### Resolved Questions +- **macOS vs FFMPosixLinux**: Sharing implementation in FFMPosixLinux works well since both are POSIX-compliant. Platform detection handles struct layout differences. +- **Struct size differences**: Using platform-specific offsets with `IS_MACOS` flag successfully handles different field sizes and layouts. + +## References + +- [JEP 454: Foreign Function & Memory API](https://openjdk.org/jeps/454) +- [JEP 471: Deprecate Memory-Access Methods in sun.misc.Unsafe](https://openjdk.org/jeps/471) +- [JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe](https://openjdk.org/jeps/498) +- [FFM API Documentation](https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html) +- [JNR-POSIX GitHub](https://github.com/jnr/jnr-posix) diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md index f5fa0a6d1..02f95e8f5 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slide-deck-plan.md @@ -47,7 +47,7 @@ - Perl compiler and runtime for the JVM - Compiles to native JVM bytecode — same as Java, Kotlin, Scala - Not an interpreter wrapping a Perl binary -- Targets Perl 5.42+ semantics. Requires Java 21+. +- Targets Perl 5.42+ semantics. Requires Java 22+. **Slide 4 — Why the JVM?** - 30 years of JIT optimization — hot code becomes native machine code diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md index f5aab5a71..f2093bda2 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md @@ -34,7 +34,7 @@ This is a common scenario in enterprise environments. Many companies have large - Not an interpreter wrapping a Perl binary - Targets Perl 5.42+ semantics -**Requires:** Java 21+ (any JDK) +**Requires:** Java 22+ (any JDK) Note: PerlOnJava generates real JVM bytecode — the same kind of instructions that javac produces. This means Perl code gets all the JVM benefits: cross-platform, Java library access, JVM tooling. diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md index ade4ec656..bf575e75b 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides.md @@ -35,7 +35,7 @@ A common scenario in enterprise environments. Large Perl codebases that work wel - Not an interpreter wrapping a Perl binary - Targets **Perl 5.42+** semantics -**Requires:** Java 21+ (any JDK) +**Requires:** Java 22+ (any JDK) Note: PerlOnJava generates real JVM bytecode — the same instructions that javac produces. Perl code gets all JVM benefits: cross-platform execution, Java library access, JVM tooling. diff --git a/docs/getting-started/docker.md b/docs/getting-started/docker.md index e89ab0b32..76c589cc8 100644 --- a/docs/getting-started/docker.md +++ b/docs/getting-started/docker.md @@ -85,7 +85,7 @@ Modify the `Dockerfile` to include additional dependencies: ```dockerfile # Add JDBC driver -FROM eclipse-temurin:21-jdk +FROM eclipse-temurin:22-jdk COPY --from=build /app/target/perlonjava-5.42.0.jar /app/perlonjava.jar COPY path/to/driver.jar /app/drivers/ ENV CLASSPATH=/app/drivers/driver.jar diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 1b0edb4e5..978ca4179 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -23,7 +23,7 @@ - [Important Notes](#important-notes) ## Prerequisites -- Java 21 or higher +- Java 22 or higher - Maven or Gradle - Optional: JDBC drivers for database connectivity @@ -148,7 +148,7 @@ See [Database Access Guide](../guides/database-access.md) for detailed connectio ## Build Notes - Maven builds use `maven-shade-plugin` for creating the shaded JAR - Gradle builds use `com.github.johnrengelman.shadow` plugin -- Both configurations target Java 21 +- Both configurations target Java 22 ## Java Library Upgrades diff --git a/docs/guides/database-access.md b/docs/guides/database-access.md index 89fa0edbd..ca0de9dd6 100644 --- a/docs/guides/database-access.md +++ b/docs/guides/database-access.md @@ -50,7 +50,7 @@ gradle clean build Calling java directly with the classpath is also possible: ```bash - java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -cp "jdbc-drivers/h2-2.2.224.jar:target/perlonjava-5.42.0.jar" org.perlonjava.app.cli.Main myscript.pl + java --enable-native-access=ALL-UNNAMED -cp "jdbc-drivers/h2-2.2.224.jar:target/perlonjava-5.42.0.jar" org.perlonjava.app.cli.Main myscript.pl ``` ## Database Connection Examples diff --git a/docs/guides/java-integration.md b/docs/guides/java-integration.md index d2498348b..d7d9df4e7 100644 --- a/docs/guides/java-integration.md +++ b/docs/guides/java-integration.md @@ -175,7 +175,7 @@ dependencies { 3. Add to your classpath: ```bash javac -cp perlonjava-5.42.0-all.jar YourApp.java - java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -cp .:perlonjava-5.42.0-all.jar YourApp + java --enable-native-access=ALL-UNNAMED -cp .:perlonjava-5.42.0-all.jar YourApp ``` ## Use Cases @@ -258,7 +258,7 @@ See also: If `getEngineByName("perl")` returns null: 1. Ensure `perlonjava-*.jar` is in classpath 2. Check `META-INF/services/javax.script.ScriptEngineFactory` exists in JAR -3. Verify Java 21 or later is being used +3. Verify Java 22 or later is being used ### ClassNotFoundException diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 01205a45d..ae76396c9 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -65,7 +65,7 @@ perl dev/tools/perl_test_runner.pl --jobs 4 --timeout 20 src/test/resources/unit ### 2. Using jprove (Standard Perl prove) -PerlOnJava includes `jprove`, a wrapper that runs the standard Perl `prove` test harness with jperl: +PerlOnJava includes `jprove` (Unix) and `jprove.bat` (Windows), wrappers that run the standard Perl `prove` test harness with jperl: ```bash # Run tests in a directory diff --git a/examples/ExifToolExample.java b/examples/ExifToolExample.java index 3d5ffc97b..de7424a2c 100644 --- a/examples/ExifToolExample.java +++ b/examples/ExifToolExample.java @@ -29,7 +29,7 @@ * 2. Compile this example: * javac -cp target/perlonjava-5.42.0.jar examples/ExifToolExample.java * 3. Run: - * java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -cp target/perlonjava-5.42.0.jar:. examples.ExifToolExample + * java --enable-native-access=ALL-UNNAMED -cp target/perlonjava-5.42.0.jar:. examples.ExifToolExample */ public class ExifToolExample { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d92d132e..b9a26ee26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ asm = "9.9.1" commons-csv = "1.14.1" fastjson2 = "2.0.61" icu4j = "78.2" -jnr-posix = "3.1.19" junit-jupiter = "6.1.0-M1" snakeyaml-engine = "3.0.1" tomlj = "1.1.1" @@ -14,13 +13,13 @@ asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" } fastjson2 = { module = "com.alibaba.fastjson2:fastjson2", version.ref = "fastjson2" } icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } -jnr-posix = { module = "com.github.jnr:jnr-posix", version.ref = "jnr-posix" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-jupiter" } snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version.ref = "snakeyaml-engine" } tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" } + [plugins] cyclonedx = "org.cyclonedx.bom:2.3.0" ospackage = "com.netflix.nebula.ospackage:12.2.0" diff --git a/jperl b/jperl index 22158a81f..0f8b6ec12 100755 --- a/jperl +++ b/jperl @@ -24,20 +24,10 @@ else fi # Determine JVM options based on Java version -# --enable-native-access=ALL-UNNAMED: Required by JNR-POSIX library for native system calls -# (file operations, process management). Can be removed if JNR-POSIX is replaced. -# --sun-misc-unsafe-memory-access=allow: Only needed for Java 21-22 to suppress JFFI warnings. -# Java 23+ uses the Foreign Function & Memory API, so this flag is not needed (and may error). +# --enable-native-access=ALL-UNNAMED: Required by FFM (Foreign Function & Memory) API +# for native system calls (file operations, process management). JVM_OPTS="--enable-native-access=ALL-UNNAMED" -# Get Java major version (e.g., "21", "22", "23", "24") -JAVA_VERSION=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+).*/\1/') - -# Add unsafe memory access flag only for Java 21-22 -if [ "$JAVA_VERSION" = "21" ] || [ "$JAVA_VERSION" = "22" ]; then - JVM_OPTS="$JVM_OPTS --sun-misc-unsafe-memory-access=allow" -fi - # Note: Only include CLASSPATH if set, to avoid empty prefix that would add current dir to path if [ -n "$CLASSPATH" ]; then CP="$CLASSPATH:$JAR_PATH" diff --git a/jperl.bat b/jperl.bat index 35b158dd0..72cd6885b 100755 --- a/jperl.bat +++ b/jperl.bat @@ -14,22 +14,9 @@ rem Set environment variable for PerlOnJava to use as $^X set PERLONJAVA_EXECUTABLE=%JPERL_PATH% rem Determine JVM options based on Java version -rem --enable-native-access=ALL-UNNAMED: Required by JNR-POSIX library for native system calls -rem (file operations, process management). Can be removed if JNR-POSIX is replaced. -rem --sun-misc-unsafe-memory-access=allow: Only needed for Java 21-22 to suppress JFFI warnings. -rem Java 23+ uses the Foreign Function & Memory API, so this flag is not needed (and may error). +rem --enable-native-access=ALL-UNNAMED: Required by FFM (Foreign Function & Memory) API +rem for native system calls (file operations, process management). set JVM_OPTS=--enable-native-access=ALL-UNNAMED -rem Get Java major version and add unsafe flag only for Java 21-22 -for /f "tokens=3" %%v in ('java -version 2^>^&1 ^| findstr /i "version"') do ( - set JAVA_VER_RAW=%%v -) -rem Remove quotes and get major version -set JAVA_VER_RAW=%JAVA_VER_RAW:"=% -for /f "delims=." %%a in ("%JAVA_VER_RAW%") do set JAVA_MAJOR=%%a - -if "%JAVA_MAJOR%"=="21" set JVM_OPTS=%JVM_OPTS% --sun-misc-unsafe-memory-access=allow -if "%JAVA_MAJOR%"=="22" set JVM_OPTS=%JVM_OPTS% --sun-misc-unsafe-memory-access=allow - rem Launch Java java %JVM_OPTS% %JPERL_OPTS% -cp "%CLASSPATH%;%SCRIPT_DIR%target\perlonjava-5.42.0.jar" org.perlonjava.app.cli.Main %* diff --git a/jprove.bat b/jprove.bat new file mode 100644 index 000000000..a15cdde6a --- /dev/null +++ b/jprove.bat @@ -0,0 +1,10 @@ +@echo off +rem jprove - Test Harness for PerlOnJava (Windows wrapper) +rem Runs the standard prove script with jperl +rem Repository: github.com/fglock/PerlOnJava + +rem Get the directory where this script is located +set SCRIPT_DIR=%~dp0 + +rem Run jperl with the prove script +call "%SCRIPT_DIR%jperl.bat" "%SCRIPT_DIR%src\main\perl\bin\prove" %* diff --git a/pom.xml b/pom.xml index d3543cb7e..9d7f38d60 100644 --- a/pom.xml +++ b/pom.xml @@ -12,8 +12,8 @@ http://maven.apache.org - 21 - 21 + 22 + 22 @@ -70,11 +70,7 @@ commons-csv 1.14.1 - - com.github.jnr - jnr-posix - 3.1.19 - + diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index eea289b00..ad6d1303a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -1014,6 +1014,11 @@ static void handleWantArrayOperator(EmitterVisitor emitterVisitor, OperatorNode emitOperator(node, emitterVisitor); } + static void handleGetppidOperator(EmitterVisitor emitterVisitor, OperatorNode node) { + emitterVisitor.pushCallContext(); + emitOperator(node, emitterVisitor); + } + static void handleUndefOperator(EmitterVisitor emitterVisitor, OperatorNode node) { if (node.operand == null) { if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java index 680944b8d..ca700c457 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java @@ -96,6 +96,7 @@ public static void emitOperatorNode(EmitterVisitor emitterVisitor, OperatorNode // Miscellaneous operators case "time", "wait" -> EmitOperator.handleTimeOperator(emitterVisitor, node); case "wantarray" -> EmitOperator.handleWantArrayOperator(emitterVisitor, node); + case "getppid" -> EmitOperator.handleGetppidOperator(emitterVisitor, node); case "undef" -> EmitOperator.handleUndefOperator(emitterVisitor, node); case "gmtime", "localtime", "reset", "select", "times" -> EmitOperator.handleTimeRelatedOperator(emitterVisitor, node); diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index f266f5e1f..fc88219f8 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "585103c87"; + public static final String gitCommitId = "5f22f5e5d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-03-25"; + public static final String gitCommitDate = "2026-03-26"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java index 4b2fb5498..438d23f23 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java @@ -3,7 +3,7 @@ import org.perlonjava.backend.bytecode.EvalStringHandler; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeBase; @@ -111,7 +111,7 @@ public static void debug(String filename, int line, InterpretedCode code, Runtim // Use POSIX isatty() to check if file descriptor 0 (stdin) is a terminal boolean isInteractive = true; try { - isInteractive = PosixLibrary.INSTANCE.isatty(0) != 0; + isInteractive = FFMPosix.get().isatty(0) != 0; } catch (Exception e) { // If isatty check fails, fall back to System.console() check isInteractive = System.console() != null; diff --git a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java index a6c7a632f..31740452f 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java @@ -1,7 +1,8 @@ package org.perlonjava.runtime.nativ; -import jnr.posix.Passwd; import org.perlonjava.frontend.parser.StringParser; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; +import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; import org.perlonjava.runtime.runtimetypes.*; import java.net.InetAddress; @@ -40,19 +41,19 @@ public static RuntimeScalar getlogin(int ctx, RuntimeBase... args) { } } - private static RuntimeList passwdToList(Passwd pw) { + private static RuntimeList passwdToList(FFMPosixInterface.PasswdEntry pw) { if (pw == null) return new RuntimeList(); RuntimeArray result = new RuntimeArray(); - String name = pw.getLoginName(); - String passwd = pw.getPassword(); - int uid = (int) pw.getUID(); - int gid = (int) pw.getGID(); + String name = pw.name(); + String passwd = pw.passwd(); + int uid = pw.uid(); + int gid = pw.gid(); if (IS_MAC) { - long change = pw.getPasswdChangeTime(); - String gecos = pw.getGECOS(); - String dir = pw.getHome(); - String shell = pw.getShell(); - long expire = pw.getExpire(); + long change = pw.change(); + String gecos = pw.gecos(); + String dir = pw.dir(); + String shell = pw.shell(); + long expire = pw.expire(); RuntimeArray.push(result, new RuntimeScalar(name)); RuntimeArray.push(result, new RuntimeScalar(passwd)); RuntimeArray.push(result, new RuntimeScalar(uid)); @@ -64,9 +65,9 @@ private static RuntimeList passwdToList(Passwd pw) { RuntimeArray.push(result, new RuntimeScalar(shell)); RuntimeArray.push(result, new RuntimeScalar(expire)); } else { - String gecos = pw.getGECOS(); - String dir = pw.getHome(); - String shell = pw.getShell(); + String gecos = pw.gecos(); + String dir = pw.dir(); + String shell = pw.shell(); RuntimeArray.push(result, new RuntimeScalar(name)); RuntimeArray.push(result, new RuntimeScalar(passwd)); RuntimeArray.push(result, new RuntimeScalar(uid)); @@ -82,12 +83,12 @@ private static RuntimeList passwdToList(Passwd pw) { } private static RuntimeList nativeGetpwnam(String username) { - Passwd pw = PosixLibrary.INSTANCE.getpwnam(username); + FFMPosixInterface.PasswdEntry pw = FFMPosix.get().getpwnam(username); return passwdToList(pw); } private static RuntimeList nativeGetpwuid(int uid) { - Passwd pw = PosixLibrary.INSTANCE.getpwuid(uid); + FFMPosixInterface.PasswdEntry pw = FFMPosix.get().getpwuid(uid); return passwdToList(pw); } @@ -100,8 +101,8 @@ public static RuntimeList getpwnam(int ctx, RuntimeBase... args) { } try { if (ctx == RuntimeContextType.SCALAR) { - Passwd pw = PosixLibrary.INSTANCE.getpwnam(username); - if (pw != null) return new RuntimeScalar((int) pw.getUID()).getList(); + FFMPosixInterface.PasswdEntry pw = FFMPosix.get().getpwnam(username); + if (pw != null) return new RuntimeScalar(pw.uid()).getList(); return new RuntimeList(); } return nativeGetpwnam(username); @@ -119,8 +120,8 @@ public static RuntimeList getpwuid(int ctx, RuntimeBase... args) { } try { if (ctx == RuntimeContextType.SCALAR) { - Passwd pw = PosixLibrary.INSTANCE.getpwuid(uid); - if (pw != null) return new RuntimeScalar(pw.getLoginName()).getList(); + FFMPosixInterface.PasswdEntry pw = FFMPosix.get().getpwuid(uid); + if (pw != null) return new RuntimeScalar(pw.name()).getList(); return new RuntimeList(); } return nativeGetpwuid(uid); @@ -213,10 +214,10 @@ public static RuntimeArray getgrgid(int ctx, RuntimeBase... args) { public static RuntimeList getpwent(int ctx, RuntimeBase... args) { if (IS_WINDOWS) return new RuntimeList(); try { - Passwd pw = PosixLibrary.INSTANCE.getpwent(); + FFMPosixInterface.PasswdEntry pw = FFMPosix.get().getpwent(); if (pw == null) return new RuntimeList(); if (ctx == RuntimeContextType.SCALAR) { - return new RuntimeScalar(pw.getLoginName()).getList(); + return new RuntimeScalar(pw.name()).getList(); } return passwdToList(pw); } catch (Exception e) { @@ -242,7 +243,7 @@ public static RuntimeArray getgrent(int ctx, RuntimeBase... args) { public static RuntimeScalar setpwent(int ctx, RuntimeBase... args) { if (!IS_WINDOWS) { try { - PosixLibrary.INSTANCE.setpwent(); + FFMPosix.get().setpwent(); } catch (Exception e) { } } @@ -260,7 +261,7 @@ public static RuntimeScalar setgrent(int ctx, RuntimeBase... args) { public static RuntimeScalar endpwent(int ctx, RuntimeBase... args) { if (!IS_WINDOWS) { try { - PosixLibrary.INSTANCE.endpwent(); + FFMPosix.get().endpwent(); } catch (Exception e) { } } diff --git a/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java b/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java index 314f7cdc3..54645dc6c 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java +++ b/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java @@ -1,5 +1,7 @@ package org.perlonjava.runtime.nativ; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; +import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeIO; @@ -14,9 +16,8 @@ public class NativeUtils { public static final boolean IS_WINDOWS = System.getProperty("os.name", "").toLowerCase().contains("win"); public static final boolean IS_MAC = System.getProperty("os.name", "").toLowerCase().contains("mac"); - private static final int DEFAULT_UID = 1000; - private static final int DEFAULT_GID = 1000; - private static final int ID_RANGE = 65536; + // FFM POSIX implementation (handles platform-specific logic internally) + private static final FFMPosixInterface posix = FFMPosix.get(); public static RuntimeScalar symlink(int ctx, RuntimeBase... args) { if (args.length < 2) { @@ -79,71 +80,30 @@ public static RuntimeScalar link(int ctx, RuntimeBase... args) { } try { - if (IS_WINDOWS) { - Files.createLink(Paths.get(newFile), Paths.get(oldFile)); - return new RuntimeScalar(1); - } else { - int result = PosixLibrary.INSTANCE.link(oldFile, newFile); - return new RuntimeScalar(result == 0 ? 1 : 0); - } + int result = posix.link(oldFile, newFile); + return new RuntimeScalar(result == 0 ? 1 : 0); } catch (Exception e) { return new RuntimeScalar(0); } } public static RuntimeScalar getppid(int ctx, RuntimeBase... args) { - if (IS_WINDOWS) { - return ProcessHandle.current().parent() - .map(ph -> new RuntimeScalar(ph.pid())) - .orElse(new RuntimeScalar(0)); - } else { - return new RuntimeScalar(PosixLibrary.INSTANCE.getppid()); - } + return new RuntimeScalar(posix.getppid()); } public static RuntimeScalar getuid(int ctx, RuntimeBase... args) { - if (IS_WINDOWS) { - try { - String userName = System.getProperty("user.name"); - if (userName != null && !userName.isEmpty()) { - return new RuntimeScalar(Math.abs(userName.hashCode()) % ID_RANGE); - } - } catch (Exception e) { - } - return new RuntimeScalar(DEFAULT_UID); - } else { - return new RuntimeScalar(PosixLibrary.INSTANCE.getuid()); - } + return new RuntimeScalar(posix.getuid()); } public static RuntimeScalar geteuid(int ctx, RuntimeBase... args) { - if (IS_WINDOWS) { - return getuid(ctx, args); - } else { - return new RuntimeScalar(PosixLibrary.INSTANCE.geteuid()); - } + return new RuntimeScalar(posix.geteuid()); } public static RuntimeScalar getgid(int ctx, RuntimeBase... args) { - if (IS_WINDOWS) { - try { - String computerName = System.getenv("COMPUTERNAME"); - if (computerName != null && !computerName.isEmpty()) { - return new RuntimeScalar(Math.abs(computerName.hashCode()) % ID_RANGE); - } - } catch (Exception e) { - } - return new RuntimeScalar(DEFAULT_GID); - } else { - return new RuntimeScalar(PosixLibrary.INSTANCE.getgid()); - } + return new RuntimeScalar(posix.getgid()); } public static RuntimeScalar getegid(int ctx, RuntimeBase... args) { - if (IS_WINDOWS) { - return getgid(ctx, args); - } else { - return new RuntimeScalar(PosixLibrary.INSTANCE.getegid()); - } + return new RuntimeScalar(posix.getegid()); } } diff --git a/src/main/java/org/perlonjava/runtime/nativ/PosixLibrary.java b/src/main/java/org/perlonjava/runtime/nativ/PosixLibrary.java index eeed920d1..e61c98386 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/PosixLibrary.java +++ b/src/main/java/org/perlonjava/runtime/nativ/PosixLibrary.java @@ -1,8 +1,40 @@ package org.perlonjava.runtime.nativ; -import jnr.posix.POSIX; -import jnr.posix.POSIXFactory; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; +import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; +/** + * POSIX library wrapper providing native system call access. + * + *

This class uses Java's Foreign Function & Memory (FFM) API for native access, + * which was finalized in Java 22. This approach eliminates the sun.misc.Unsafe + * warnings that appeared on Java 24+ with the previous JNR-POSIX dependency.

+ * + *

Usage

+ *
{@code
+ * FFMPosixInterface posix = PosixLibrary.getFFM();
+ * int result = posix.kill(pid, signal);
+ * }
+ * + * @see FFMPosix + * @see FFMPosixInterface + */ public class PosixLibrary { - public static final POSIX INSTANCE = POSIXFactory.getNativePOSIX(); + + /** + * Check if the FFM implementation is enabled. + * FFM is now enabled by default. + * @return true if FFM is enabled + */ + public static boolean isFFMEnabled() { + return FFMPosix.isEnabled(); + } + + /** + * Get the FFM POSIX implementation. + * @return Platform-specific FFM POSIX implementation + */ + public static FFMPosixInterface getFFM() { + return FFMPosix.get(); + } } diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosix.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosix.java new file mode 100644 index 000000000..9a527feb3 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosix.java @@ -0,0 +1,133 @@ +package org.perlonjava.runtime.nativ.ffm; + +/** + * Factory for creating platform-specific FFM POSIX implementations. + * + *

This class detects the current operating system and returns the appropriate + * implementation of {@link FFMPosixInterface}.

+ * + *

Feature Flag

+ *

The FFM implementation is controlled by the system property {@code perlonjava.ffm.enabled}. + * When set to {@code true}, the FFM implementation is used instead of JNR-POSIX. + * Default is {@code false} (use JNR-POSIX).

+ * + *

Usage

+ *
{@code
+ * // Check if FFM is enabled
+ * if (FFMPosix.isEnabled()) {
+ *     FFMPosixInterface posix = FFMPosix.get();
+ *     int result = posix.kill(pid, signal);
+ * }
+ * }
+ * + *

Supported Platforms

+ *
    + *
  • Linux (x86_64, aarch64)
  • + *
  • macOS (x86_64, aarch64/Apple Silicon)
  • + *
  • Windows (x86_64) - limited POSIX emulation
  • + *
+ */ +public final class FFMPosix { + + /** + * System property to enable FFM implementation. + * Set to "true" to use FFM instead of JNR-POSIX. + */ + public static final String FFM_ENABLED_PROPERTY = "perlonjava.ffm.enabled"; + + /** + * Environment variable alternative to enable FFM. + */ + public static final String FFM_ENABLED_ENV = "PERLONJAVA_FFM_ENABLED"; + + private static final FFMPosixInterface INSTANCE; + private static final boolean ENABLED; + private static final String OS_NAME; + private static final String OS_ARCH; + + static { + OS_NAME = System.getProperty("os.name", "").toLowerCase(); + OS_ARCH = System.getProperty("os.arch", "").toLowerCase(); + + // FFM is now enabled by default (JNR-POSIX migration complete) + // Can be disabled via system property or environment variable for testing + String sysProp = System.getProperty(FFM_ENABLED_PROPERTY); + String envVar = System.getenv(FFM_ENABLED_ENV); + // Default to true unless explicitly set to false + ENABLED = !"false".equalsIgnoreCase(sysProp) && + (sysProp != null || !"false".equalsIgnoreCase(envVar)); + + INSTANCE = createInstance(); + } + + private FFMPosix() { + // Prevent instantiation + } + + /** + * Check if FFM implementation is enabled. + * @return true if FFM is enabled via system property or environment variable + */ + public static boolean isEnabled() { + return ENABLED; + } + + /** + * Get the platform-specific FFM POSIX implementation. + * @return FFMPosixInterface implementation for the current platform + */ + public static FFMPosixInterface get() { + return INSTANCE; + } + + /** + * Get the detected operating system name. + * @return Operating system name (lowercase) + */ + public static String getOsName() { + return OS_NAME; + } + + /** + * Get the detected CPU architecture. + * @return CPU architecture (lowercase) + */ + public static String getOsArch() { + return OS_ARCH; + } + + /** + * Check if running on Windows. + * @return true if Windows + */ + public static boolean isWindows() { + return OS_NAME.contains("win"); + } + + /** + * Check if running on macOS. + * @return true if macOS + */ + public static boolean isMacOS() { + return OS_NAME.contains("mac"); + } + + /** + * Check if running on Linux. + * @return true if Linux + */ + public static boolean isLinux() { + return OS_NAME.contains("linux"); + } + + private static FFMPosixInterface createInstance() { + if (isWindows()) { + return new FFMPosixWindows(); + } else if (isMacOS()) { + return new FFMPosixMacOS(); + } else { + // Default to Linux for Unix-like systems + return new FFMPosixLinux(); + } + } +} diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java new file mode 100644 index 000000000..62267d67b --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java @@ -0,0 +1,218 @@ +package org.perlonjava.runtime.nativ.ffm; + +/** + * Interface defining POSIX functions to be implemented via Java FFM API. + * This interface abstracts platform-specific native calls, allowing different + * implementations for Linux, macOS, and Windows. + * + *

All methods should handle errors by returning appropriate error codes + * and setting errno via {@link #errno()} / {@link #setErrno(int)}.

+ */ +public interface FFMPosixInterface { + + // ==================== Process Functions ==================== + + /** + * Send a signal to a process. + * @param pid Process ID (negative for process group) + * @param signal Signal number + * @return 0 on success, -1 on error (check errno) + */ + int kill(int pid, int signal); + + /** + * Get the parent process ID. + * @return Parent process ID + */ + int getppid(); + + /** + * Wait for a child process. + * @param pid Process ID to wait for (-1 for any child) + * @param status Array to store exit status (at least 1 element) + * @param options Wait options (WNOHANG, WUNTRACED, etc.) + * @return Process ID of terminated child, 0 if WNOHANG and no child, -1 on error + */ + long waitpid(int pid, int[] status, int options); + + // ==================== User/Group Functions ==================== + + /** + * Get the real user ID. + * @return User ID + */ + int getuid(); + + /** + * Get the effective user ID. + * @return Effective user ID + */ + int geteuid(); + + /** + * Get the real group ID. + * @return Group ID + */ + int getgid(); + + /** + * Get the effective group ID. + * @return Effective group ID + */ + int getegid(); + + /** + * Get password entry by username. + * @param name Username + * @return Password entry or null if not found + */ + PasswdEntry getpwnam(String name); + + /** + * Get password entry by user ID. + * @param uid User ID + * @return Password entry or null if not found + */ + PasswdEntry getpwuid(int uid); + + /** + * Get next password entry (for iteration). + * @return Next password entry or null if end of database + */ + PasswdEntry getpwent(); + + /** + * Reset password database to beginning. + */ + void setpwent(); + + /** + * Close password database. + */ + void endpwent(); + + // ==================== File Functions ==================== + + /** + * Get file status. + * @param path File path + * @return Stat result or null on error (check errno) + */ + StatResult stat(String path); + + /** + * Get file status (don't follow symlinks). + * @param path File path + * @return Stat result or null on error (check errno) + */ + StatResult lstat(String path); + + /** + * Change file permissions. + * @param path File path + * @param mode Permission mode (octal) + * @return 0 on success, -1 on error + */ + int chmod(String path, int mode); + + /** + * Create a hard link. + * @param oldPath Existing file path + * @param newPath New link path + * @return 0 on success, -1 on error + */ + int link(String oldPath, String newPath); + + /** + * Set file access and modification times. + * @param path File path + * @param atime Access time (seconds since epoch) + * @param mtime Modification time (seconds since epoch) + * @return 0 on success, -1 on error + */ + int utimes(String path, long atime, long mtime); + + // ==================== Terminal Functions ==================== + + /** + * Check if file descriptor is a terminal. + * @param fd File descriptor (0=stdin, 1=stdout, 2=stderr) + * @return 1 if terminal, 0 if not + */ + int isatty(int fd); + + // ==================== File Control Functions ==================== + + /** + * File control operations. + * @param fd File descriptor + * @param cmd Command (F_GETFL, F_SETFL, etc.) + * @param arg Command argument + * @return Result depends on command, -1 on error + */ + int fcntl(int fd, int cmd, int arg); + + /** + * Get/set file creation mask. + * @param mask New umask value + * @return Previous umask value + */ + int umask(int mask); + + // ==================== Error Handling ==================== + + /** + * Get the last error number. + * @return errno value + */ + int errno(); + + /** + * Set the error number (for testing/simulation). + * @param errno Error number to set + */ + void setErrno(int errno); + + /** + * Get error message for errno. + * @param errno Error number + * @return Error message string + */ + String strerror(int errno); + + // ==================== Data Structures ==================== + + /** + * Password database entry (struct passwd equivalent). + */ + record PasswdEntry( + String name, // pw_name - username + String passwd, // pw_passwd - password (usually "x") + int uid, // pw_uid - user ID + int gid, // pw_gid - group ID + String gecos, // pw_gecos - user info (real name, etc.) + String dir, // pw_dir - home directory + String shell, // pw_shell - login shell + long change, // pw_change - password change time (BSD/macOS) + long expire // pw_expire - account expiration (BSD/macOS) + ) {} + + /** + * File status result (struct stat equivalent). + */ + record StatResult( + long dev, // st_dev - device ID + long ino, // st_ino - inode number + int mode, // st_mode - file mode (type and permissions) + long nlink, // st_nlink - number of hard links + int uid, // st_uid - owner user ID + int gid, // st_gid - owner group ID + long rdev, // st_rdev - device ID (if special file) + long size, // st_size - file size in bytes + long atime, // st_atime - last access time + long mtime, // st_mtime - last modification time + long ctime, // st_ctime - last status change time + long blksize, // st_blksize - preferred I/O block size + long blocks // st_blocks - number of 512-byte blocks + ) {} +} diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java new file mode 100644 index 000000000..28a34a463 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java @@ -0,0 +1,710 @@ +package org.perlonjava.runtime.nativ.ffm; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Linux/macOS implementation of FFM POSIX interface. + * + *

This class uses Java's Foreign Function & Memory (FFM) API to call + * native Linux/glibc functions directly, without JNR-POSIX.

+ */ +public class FFMPosixLinux implements FFMPosixInterface { + + // Platform detection + private static final boolean IS_MACOS = System.getProperty("os.name").toLowerCase().contains("mac"); + private static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().contains("linux"); + + // Thread-local errno storage (used as fallback) + private static final ThreadLocal threadErrno = ThreadLocal.withInitial(() -> 0); + + // Lazy-initialized FFM components + private static volatile boolean initialized = false; + private static Linker linker; + private static SymbolLookup stdlib; + + // Method handles for simple functions (no errno capture needed) + private static MethodHandle getuidHandle; + private static MethodHandle geteuidHandle; + private static MethodHandle getgidHandle; + private static MethodHandle getegidHandle; + private static MethodHandle getppidHandle; + private static MethodHandle isattyHandle; + private static MethodHandle umaskHandle; + + // Method handles that need errno capture + private static MethodHandle killHandle; + private static MethodHandle chmodHandle; + private static MethodHandle statHandle; + private static MethodHandle lstatHandle; + + // Method handles for passwd functions + private static MethodHandle getpwnamHandle; + private static MethodHandle getpwuidHandle; + private static MethodHandle getpwentHandle; + private static MethodHandle setpwentHandle; + private static MethodHandle endpwentHandle; + + // Linker options for errno capture + private static Linker.Option captureErrno; + private static long errnoOffset; + + // Struct stat size and field offsets (platform-dependent) + private static int STAT_SIZE; + private static long ST_DEV_OFFSET; + private static long ST_INO_OFFSET; + private static long ST_MODE_OFFSET; + private static long ST_NLINK_OFFSET; + private static long ST_UID_OFFSET; + private static long ST_GID_OFFSET; + private static long ST_RDEV_OFFSET; + private static long ST_SIZE_OFFSET; + private static long ST_BLKSIZE_OFFSET; + private static long ST_BLOCKS_OFFSET; + private static long ST_ATIME_OFFSET; + private static long ST_MTIME_OFFSET; + private static long ST_CTIME_OFFSET; + + // Struct passwd field offsets (platform-dependent) + private static long PW_NAME_OFFSET; + private static long PW_PASSWD_OFFSET; + private static long PW_UID_OFFSET; + private static long PW_GID_OFFSET; + private static long PW_CHANGE_OFFSET; // macOS only + private static long PW_CLASS_OFFSET; // macOS only + private static long PW_GECOS_OFFSET; + private static long PW_DIR_OFFSET; + private static long PW_SHELL_OFFSET; + private static long PW_EXPIRE_OFFSET; // macOS only + + /** + * Initialize FFM components lazily. + */ + private static synchronized void ensureInitialized() { + if (initialized) return; + + try { + linker = Linker.nativeLinker(); + stdlib = linker.defaultLookup(); + + // Set up errno capture + captureErrno = Linker.Option.captureCallState("errno"); + errnoOffset = Linker.Option.captureStateLayout() + .byteOffset(java.lang.foreign.MemoryLayout.PathElement.groupElement("errno")); + + // Initialize platform-specific struct stat offsets + initStatOffsets(); + + // Simple functions (return value only, no errno) + getuidHandle = linker.downcallHandle( + stdlib.find("getuid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT) + ); + + geteuidHandle = linker.downcallHandle( + stdlib.find("geteuid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT) + ); + + getgidHandle = linker.downcallHandle( + stdlib.find("getgid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT) + ); + + getegidHandle = linker.downcallHandle( + stdlib.find("getegid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT) + ); + + getppidHandle = linker.downcallHandle( + stdlib.find("getppid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT) + ); + + isattyHandle = linker.downcallHandle( + stdlib.find("isatty").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + umaskHandle = linker.downcallHandle( + stdlib.find("umask").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + // Functions that need errno capture + killHandle = linker.downcallHandle( + stdlib.find("kill").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), + captureErrno + ); + + chmodHandle = linker.downcallHandle( + stdlib.find("chmod").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT), + captureErrno + ); + + // stat and lstat - take path pointer and stat buffer pointer + statHandle = linker.downcallHandle( + stdlib.find("stat").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS), + captureErrno + ); + + lstatHandle = linker.downcallHandle( + stdlib.find("lstat").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS), + captureErrno + ); + + // passwd functions - return pointers to static passwd struct + getpwnamHandle = linker.downcallHandle( + stdlib.find("getpwnam").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS) + ); + + getpwuidHandle = linker.downcallHandle( + stdlib.find("getpwuid").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT) + ); + + getpwentHandle = linker.downcallHandle( + stdlib.find("getpwent").orElseThrow(), + FunctionDescriptor.of(ValueLayout.ADDRESS) + ); + + setpwentHandle = linker.downcallHandle( + stdlib.find("setpwent").orElseThrow(), + FunctionDescriptor.ofVoid() + ); + + endpwentHandle = linker.downcallHandle( + stdlib.find("endpwent").orElseThrow(), + FunctionDescriptor.ofVoid() + ); + + // Initialize passwd struct offsets + initPasswdOffsets(); + + initialized = true; + } catch (Throwable e) { + throw new RuntimeException("Failed to initialize FFM POSIX bindings", e); + } + } + + /** + * Initialize platform-specific struct stat field offsets. + * + * These offsets are determined by the C struct layout on each platform. + * Linux x86_64 and macOS x86_64/arm64 have different layouts. + */ + private static void initStatOffsets() { + if (IS_MACOS) { + // macOS struct stat layout (64-bit) + // Based on sys/stat.h from macOS SDK + STAT_SIZE = 144; + ST_DEV_OFFSET = 0; // int32_t st_dev + ST_MODE_OFFSET = 4; // mode_t st_mode (uint16_t) + ST_NLINK_OFFSET = 6; // nlink_t st_nlink (uint16_t) + ST_INO_OFFSET = 8; // ino_t st_ino (uint64_t) + ST_UID_OFFSET = 16; // uid_t st_uid + ST_GID_OFFSET = 20; // gid_t st_gid + ST_RDEV_OFFSET = 24; // dev_t st_rdev + // struct timespec st_atimespec at offset 32 (16 bytes: sec + nsec) + ST_ATIME_OFFSET = 32; // time_t st_atime + // struct timespec st_mtimespec at offset 48 + ST_MTIME_OFFSET = 48; // time_t st_mtime + // struct timespec st_ctimespec at offset 64 + ST_CTIME_OFFSET = 64; // time_t st_ctime + // struct timespec st_birthtimespec at offset 80 + ST_SIZE_OFFSET = 96; // off_t st_size + ST_BLOCKS_OFFSET = 104; // blkcnt_t st_blocks + ST_BLKSIZE_OFFSET = 112; // blksize_t st_blksize + } else { + // Linux x86_64 struct stat layout + // Based on /usr/include/bits/stat.h + STAT_SIZE = 144; + ST_DEV_OFFSET = 0; // dev_t st_dev (8 bytes) + ST_INO_OFFSET = 8; // ino_t st_ino (8 bytes) + ST_NLINK_OFFSET = 16; // nlink_t st_nlink (8 bytes) + ST_MODE_OFFSET = 24; // mode_t st_mode (4 bytes) + ST_UID_OFFSET = 28; // uid_t st_uid (4 bytes) + ST_GID_OFFSET = 32; // gid_t st_gid (4 bytes) + // 4 bytes padding + ST_RDEV_OFFSET = 40; // dev_t st_rdev (8 bytes) + ST_SIZE_OFFSET = 48; // off_t st_size (8 bytes) + ST_BLKSIZE_OFFSET = 56; // blksize_t st_blksize (8 bytes) + ST_BLOCKS_OFFSET = 64; // blkcnt_t st_blocks (8 bytes) + // struct timespec st_atim at offset 72 (16 bytes) + ST_ATIME_OFFSET = 72; + // struct timespec st_mtim at offset 88 + ST_MTIME_OFFSET = 88; + // struct timespec st_ctim at offset 104 + ST_CTIME_OFFSET = 104; + } + } + + /** + * Initialize platform-specific struct passwd field offsets. + * + * The struct passwd layout differs between Linux and macOS. + * - Linux: simpler struct with just the basics + * - macOS: includes additional fields like pw_change, pw_class, pw_expire + */ + private static void initPasswdOffsets() { + // Pointer size on 64-bit systems + int ptrSize = 8; + + if (IS_MACOS) { + // macOS struct passwd layout (from pwd.h) + // struct passwd { + // char *pw_name; // 0 + // char *pw_passwd; // 8 + // uid_t pw_uid; // 16 (4 bytes) + // gid_t pw_gid; // 20 (4 bytes) + // time_t pw_change; // 24 (8 bytes) - macOS specific + // char *pw_class; // 32 (pointer) - macOS specific + // char *pw_gecos; // 40 + // char *pw_dir; // 48 + // char *pw_shell; // 56 + // time_t pw_expire; // 64 (8 bytes) - macOS specific + // int pw_fields; // 72 (4 bytes) - macOS specific + // }; + PW_NAME_OFFSET = 0; + PW_PASSWD_OFFSET = 8; + PW_UID_OFFSET = 16; + PW_GID_OFFSET = 20; + PW_CHANGE_OFFSET = 24; + PW_CLASS_OFFSET = 32; + PW_GECOS_OFFSET = 40; + PW_DIR_OFFSET = 48; + PW_SHELL_OFFSET = 56; + PW_EXPIRE_OFFSET = 64; + } else { + // Linux struct passwd layout (from pwd.h) + // struct passwd { + // char *pw_name; // 0 + // char *pw_passwd; // 8 + // uid_t pw_uid; // 16 (4 bytes) + // gid_t pw_gid; // 20 (4 bytes) + // char *pw_gecos; // 24 + // char *pw_dir; // 32 + // char *pw_shell; // 40 + // }; + PW_NAME_OFFSET = 0; + PW_PASSWD_OFFSET = 8; + PW_UID_OFFSET = 16; + PW_GID_OFFSET = 20; + PW_GECOS_OFFSET = 24; + PW_DIR_OFFSET = 32; + PW_SHELL_OFFSET = 40; + PW_CHANGE_OFFSET = -1; // Not available on Linux + PW_CLASS_OFFSET = -1; // Not available on Linux + PW_EXPIRE_OFFSET = -1; // Not available on Linux + } + } + + // ==================== Process Functions ==================== + + @Override + public int kill(int pid, int signal) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) killHandle.invokeExact(capturedState, pid, signal); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(1); // EPERM + return -1; + } + } + + @Override + public int getppid() { + ensureInitialized(); + try { + return (int) getppidHandle.invokeExact(); + } catch (Throwable e) { + return 1; // Return init's PID as fallback + } + } + + @Override + public long waitpid(int pid, int[] status, int options) { + // TODO: Implement with FFM in Phase 3 + throw new UnsupportedOperationException("FFM waitpid() not yet implemented - use JNR-POSIX"); + } + + // ==================== User/Group Functions ==================== + + @Override + public int getuid() { + ensureInitialized(); + try { + return (int) getuidHandle.invokeExact(); + } catch (Throwable e) { + return -1; + } + } + + @Override + public int geteuid() { + ensureInitialized(); + try { + return (int) geteuidHandle.invokeExact(); + } catch (Throwable e) { + return -1; + } + } + + @Override + public int getgid() { + ensureInitialized(); + try { + return (int) getgidHandle.invokeExact(); + } catch (Throwable e) { + return -1; + } + } + + @Override + public int getegid() { + ensureInitialized(); + try { + return (int) getegidHandle.invokeExact(); + } catch (Throwable e) { + return -1; + } + } + + @Override + public PasswdEntry getpwnam(String name) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment nameSegment = arena.allocateFrom(name); + MemorySegment result = (MemorySegment) getpwnamHandle.invokeExact(nameSegment); + if (result.address() == 0) { + return null; + } + return readPasswdEntry(result); + } catch (Throwable e) { + return null; + } + } + + @Override + public PasswdEntry getpwuid(int uid) { + ensureInitialized(); + try { + MemorySegment result = (MemorySegment) getpwuidHandle.invokeExact(uid); + if (result.address() == 0) { + return null; + } + return readPasswdEntry(result); + } catch (Throwable e) { + return null; + } + } + + @Override + public PasswdEntry getpwent() { + ensureInitialized(); + try { + MemorySegment result = (MemorySegment) getpwentHandle.invokeExact(); + if (result.address() == 0) { + return null; + } + return readPasswdEntry(result); + } catch (Throwable e) { + return null; + } + } + + @Override + public void setpwent() { + ensureInitialized(); + try { + setpwentHandle.invokeExact(); + } catch (Throwable e) { + // Ignore errors + } + } + + @Override + public void endpwent() { + ensureInitialized(); + try { + endpwentHandle.invokeExact(); + } catch (Throwable e) { + // Ignore errors + } + } + + /** + * Read a passwd entry from a native struct pointer. + */ + private PasswdEntry readPasswdEntry(MemorySegment passwdPtr) { + // Reinterpret the pointer as having enough size to read all fields + MemorySegment passwd = passwdPtr.reinterpret(128); // Should be big enough for the struct + + // Read string pointers and convert to Java strings + String name = readCString(passwd.get(ValueLayout.ADDRESS, PW_NAME_OFFSET)); + String passwdField = readCString(passwd.get(ValueLayout.ADDRESS, PW_PASSWD_OFFSET)); + int uid = passwd.get(ValueLayout.JAVA_INT, PW_UID_OFFSET); + int gid = passwd.get(ValueLayout.JAVA_INT, PW_GID_OFFSET); + String gecos = readCString(passwd.get(ValueLayout.ADDRESS, PW_GECOS_OFFSET)); + String dir = readCString(passwd.get(ValueLayout.ADDRESS, PW_DIR_OFFSET)); + String shell = readCString(passwd.get(ValueLayout.ADDRESS, PW_SHELL_OFFSET)); + + // macOS-specific fields + long change = 0; + long expire = 0; + if (IS_MACOS && PW_CHANGE_OFFSET >= 0) { + change = passwd.get(ValueLayout.JAVA_LONG, PW_CHANGE_OFFSET); + expire = passwd.get(ValueLayout.JAVA_LONG, PW_EXPIRE_OFFSET); + } + + return new PasswdEntry(name, passwdField, uid, gid, gecos, dir, shell, change, expire); + } + + /** + * Read a C string from a native pointer. + */ + private String readCString(MemorySegment ptr) { + if (ptr.address() == 0) { + return ""; + } + // Reinterpret with max size to find null terminator + return ptr.reinterpret(1024).getString(0); + } + + // ==================== File Functions ==================== + + @Override + public StatResult stat(String path) { + return statInternal(path, true); + } + + @Override + public StatResult lstat(String path) { + return statInternal(path, false); + } + + /** + * Internal stat/lstat implementation. + * @param path File path + * @param followLinks true for stat, false for lstat + * @return StatResult or null on error + */ + private StatResult statInternal(String path, boolean followLinks) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + // Allocate path string in native memory + MemorySegment pathSegment = arena.allocateFrom(path); + + // Allocate stat buffer + MemorySegment statBuf = arena.allocate(STAT_SIZE); + + // Allocate errno capture state + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + + // Call stat or lstat + int result; + if (followLinks) { + result = (int) statHandle.invokeExact(capturedState, pathSegment, statBuf); + } else { + result = (int) lstatHandle.invokeExact(capturedState, pathSegment, statBuf); + } + + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + return null; + } + + // Read struct fields based on platform + return readStatResult(statBuf); + } catch (Throwable e) { + setErrno(5); // EIO + return null; + } + } + + /** + * Read stat struct fields from memory segment. + */ + private StatResult readStatResult(MemorySegment statBuf) { + if (IS_MACOS) { + // macOS: some fields are smaller + return new StatResult( + statBuf.get(ValueLayout.JAVA_INT, ST_DEV_OFFSET) & 0xFFFFFFFFL, // dev (4 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_INO_OFFSET), // ino (8 bytes) + statBuf.get(ValueLayout.JAVA_SHORT, ST_MODE_OFFSET) & 0xFFFF, // mode (2 bytes) + statBuf.get(ValueLayout.JAVA_SHORT, ST_NLINK_OFFSET) & 0xFFFFL, // nlink (2 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_UID_OFFSET), // uid (4 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_GID_OFFSET), // gid (4 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_RDEV_OFFSET) & 0xFFFFFFFFL, // rdev (4 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_SIZE_OFFSET), // size (8 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_ATIME_OFFSET), // atime (8 bytes, sec part of timespec) + statBuf.get(ValueLayout.JAVA_LONG, ST_MTIME_OFFSET), // mtime + statBuf.get(ValueLayout.JAVA_LONG, ST_CTIME_OFFSET), // ctime + statBuf.get(ValueLayout.JAVA_INT, ST_BLKSIZE_OFFSET) & 0xFFFFFFFFL, // blksize (4 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_BLOCKS_OFFSET) // blocks (8 bytes) + ); + } else { + // Linux x86_64: larger field sizes + return new StatResult( + statBuf.get(ValueLayout.JAVA_LONG, ST_DEV_OFFSET), // dev (8 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_INO_OFFSET), // ino (8 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_MODE_OFFSET), // mode (4 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_NLINK_OFFSET), // nlink (8 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_UID_OFFSET), // uid (4 bytes) + statBuf.get(ValueLayout.JAVA_INT, ST_GID_OFFSET), // gid (4 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_RDEV_OFFSET), // rdev (8 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_SIZE_OFFSET), // size (8 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_ATIME_OFFSET), // atime + statBuf.get(ValueLayout.JAVA_LONG, ST_MTIME_OFFSET), // mtime + statBuf.get(ValueLayout.JAVA_LONG, ST_CTIME_OFFSET), // ctime + statBuf.get(ValueLayout.JAVA_LONG, ST_BLKSIZE_OFFSET), // blksize (8 bytes) + statBuf.get(ValueLayout.JAVA_LONG, ST_BLOCKS_OFFSET) // blocks (8 bytes) + ); + } + } + + @Override + public int chmod(String path, int mode) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment pathSegment = arena.allocateFrom(path); + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) chmodHandle.invokeExact(capturedState, pathSegment, mode); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(1); // EPERM + return -1; + } + } + + @Override + public int link(String oldPath, String newPath) { + // Can use Java NIO for this + try { + Files.createLink(Path.of(newPath), Path.of(oldPath)); + return 0; + } catch (Exception e) { + setErrno(getErrnoForException(e)); + return -1; + } + } + + @Override + public int utimes(String path, long atime, long mtime) { + // TODO: Implement with FFM in Phase 3 + throw new UnsupportedOperationException("FFM utimes() not yet implemented - use JNR-POSIX"); + } + + // ==================== Terminal Functions ==================== + + @Override + public int isatty(int fd) { + ensureInitialized(); + try { + return (int) isattyHandle.invokeExact(fd); + } catch (Throwable e) { + return 0; + } + } + + // ==================== File Control Functions ==================== + + @Override + public int fcntl(int fd, int cmd, int arg) { + // TODO: Implement with FFM in Phase 3 + throw new UnsupportedOperationException("FFM fcntl() not yet implemented - use JNR-POSIX"); + } + + @Override + public int umask(int mask) { + ensureInitialized(); + try { + return (int) umaskHandle.invokeExact(mask); + } catch (Throwable e) { + return 022; // Default umask + } + } + + // ==================== Error Handling ==================== + + @Override + public int errno() { + return threadErrno.get(); + } + + @Override + public void setErrno(int errno) { + threadErrno.set(errno); + } + + @Override + public String strerror(int errno) { + // Common POSIX error messages + return switch (errno) { + case 0 -> "Success"; + case 1 -> "Operation not permitted"; + case 2 -> "No such file or directory"; + case 3 -> "No such process"; + case 4 -> "Interrupted system call"; + case 5 -> "I/O error"; + case 9 -> "Bad file descriptor"; + case 10 -> "No child processes"; + case 12 -> "Out of memory"; + case 13 -> "Permission denied"; + case 17 -> "File exists"; + case 20 -> "Not a directory"; + case 21 -> "Is a directory"; + case 22 -> "Invalid argument"; + case 28 -> "No space left on device"; + case 30 -> "Read-only file system"; + default -> "Unknown error " + errno; + }; + } + + // ==================== Helper Methods ==================== + + /** + * Map Java exceptions to errno values. + */ + protected int getErrnoForException(Exception e) { + String msg = e.getMessage(); + if (msg == null) return 5; // EIO + msg = msg.toLowerCase(); + + if (msg.contains("no such file") || msg.contains("not found")) return 2; // ENOENT + if (msg.contains("permission denied") || msg.contains("access denied")) return 13; // EACCES + if (msg.contains("file exists")) return 17; // EEXIST + if (msg.contains("not a directory")) return 20; // ENOTDIR + if (msg.contains("is a directory")) return 21; // EISDIR + if (msg.contains("no space")) return 28; // ENOSPC + if (msg.contains("read-only")) return 30; // EROFS + + return 5; // EIO - generic I/O error + } +} diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixMacOS.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixMacOS.java new file mode 100644 index 000000000..48efd13c4 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixMacOS.java @@ -0,0 +1,42 @@ +package org.perlonjava.runtime.nativ.ffm; + +/** + * macOS implementation of FFM POSIX interface. + * + *

macOS is POSIX-compliant, so most functions are inherited from the Linux + * implementation. This class overrides functions where macOS differs:

+ *
    + *
  • Different struct layouts (stat, passwd have different field sizes/order)
  • + *
  • BSD-specific extensions (pw_change, pw_expire in passwd)
  • + *
  • Different library names (libc vs glibc)
  • + *
+ * + *

Note: This is a stub implementation for Phase 1. Overrides will be + * added in subsequent phases as needed.

+ */ +public class FFMPosixMacOS extends FFMPosixLinux { + + /** + * Creates a new macOS FFM POSIX implementation. + */ + public FFMPosixMacOS() { + super(); + } + + // ==================== macOS-Specific Overrides ==================== + + // Most POSIX functions are the same on macOS and Linux. + // Overrides will be added here when struct layouts differ. + + // TODO Phase 3: Override stat() with macOS-specific struct layout + // macOS uses a different stat structure with: + // - st_dev is int32_t (not dev_t = uint64_t) + // - Different field ordering + // - Additional fields like st_flags, st_gen + + // TODO Phase 3: Override getpwnam/getpwuid/getpwent for BSD passwd struct + // BSD passwd has additional fields: + // - pw_change (password change time) + // - pw_class (user access class) + // - pw_expire (account expiration time) +} diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java new file mode 100644 index 000000000..9159c94a8 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java @@ -0,0 +1,387 @@ +package org.perlonjava.runtime.nativ.ffm; + +import java.io.Console; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.DosFileAttributes; + +/** + * Windows implementation of FFM POSIX interface. + * + *

Windows is not POSIX-compliant, so this class provides Windows-specific + * implementations or emulations for POSIX functions:

+ *
    + *
  • Some functions use Windows API via FFM (kernel32.dll)
  • + *
  • Some functions use pure Java alternatives
  • + *
  • Some functions return simulated/default values
  • + *
  • Some functions are not supported (throw UnsupportedOperationException)
  • + *
+ * + *

Note: This is a stub implementation for Phase 1. Windows-specific + * implementations will be added in Phase 4.

+ */ +public class FFMPosixWindows implements FFMPosixInterface { + + // Thread-local errno storage + private static final ThreadLocal threadErrno = ThreadLocal.withInitial(() -> 0); + + // Default UID/GID values for Windows (simulated) + private static final int DEFAULT_UID = 1000; + private static final int DEFAULT_GID = 1000; + private static final int ID_RANGE = 65536; + + // Cached user info + private static final int CURRENT_UID; + private static final int CURRENT_GID; + private static final String CURRENT_USER; + + static { + CURRENT_USER = System.getProperty("user.name", "user"); + CURRENT_UID = Math.abs(CURRENT_USER.hashCode()) % ID_RANGE; + + String computerName = System.getenv("COMPUTERNAME"); + CURRENT_GID = computerName != null ? + Math.abs(computerName.hashCode()) % ID_RANGE : DEFAULT_GID; + } + + // ==================== Process Functions ==================== + + @Override + public int kill(int pid, int signal) { + // Windows implementation using ProcessHandle + try { + if (signal == 0) { + // Signal 0 = check if process exists + return ProcessHandle.of(pid).map(ph -> ph.isAlive() ? 0 : -1).orElse(-1); + } + + var proc = ProcessHandle.of(pid); + if (proc.isEmpty()) { + setErrno(3); // ESRCH - No such process + return -1; + } + + boolean destroyed = switch (signal) { + case 9 -> proc.get().destroyForcibly(); // SIGKILL + case 2, 3, 15 -> proc.get().destroy(); // SIGINT, SIGQUIT, SIGTERM + default -> { + setErrno(22); // EINVAL - not supported + yield false; + } + }; + + return destroyed ? 0 : -1; + } catch (Exception e) { + setErrno(1); // EPERM + return -1; + } + } + + @Override + public int getppid() { + return ProcessHandle.current().parent() + .map(ph -> (int) ph.pid()) + .orElse(0); + } + + @Override + public long waitpid(int pid, int[] status, int options) { + // Windows doesn't have waitpid - use ProcessHandle + try { + var proc = ProcessHandle.of(pid); + if (proc.isEmpty()) { + setErrno(10); // ECHILD + return -1; + } + + // Check WNOHANG + boolean noHang = (options & 1) != 0; + if (noHang && proc.get().isAlive()) { + return 0; + } + + proc.get().onExit().join(); + if (status != null && status.length > 0) { + status[0] = 0; // Exit status not available via ProcessHandle + } + return pid; + } catch (Exception e) { + setErrno(10); // ECHILD + return -1; + } + } + + // ==================== User/Group Functions ==================== + + @Override + public int getuid() { + return CURRENT_UID; + } + + @Override + public int geteuid() { + return CURRENT_UID; + } + + @Override + public int getgid() { + return CURRENT_GID; + } + + @Override + public int getegid() { + return CURRENT_GID; + } + + @Override + public PasswdEntry getpwnam(String name) { + if (name == null || name.isEmpty()) { + return null; + } + + // Return info for current user or simulated info + if (name.equals(CURRENT_USER)) { + return new PasswdEntry( + CURRENT_USER, + "x", + CURRENT_UID, + CURRENT_GID, + "", + System.getProperty("user.home", "C:\\Users\\" + CURRENT_USER), + "cmd.exe", + 0, + 0 + ); + } + + // Simulated entry for other users + int uid = name.equals("Administrator") ? 500 : 1001; + return new PasswdEntry( + name, + "x", + uid, + 513, + "", + "C:\\Users\\" + name, + "cmd.exe", + 0, + 0 + ); + } + + @Override + public PasswdEntry getpwuid(int uid) { + if (uid == CURRENT_UID) { + return getpwnam(CURRENT_USER); + } + return null; + } + + @Override + public PasswdEntry getpwent() { + // Not supported on Windows + return null; + } + + @Override + public void setpwent() { + // No-op on Windows + } + + @Override + public void endpwent() { + // No-op on Windows + } + + // ==================== File Functions ==================== + + @Override + public StatResult stat(String path) { + try { + Path p = Path.of(path); + BasicFileAttributes basic = Files.readAttributes(p, BasicFileAttributes.class); + DosFileAttributes dos = null; + try { + dos = Files.readAttributes(p, DosFileAttributes.class); + } catch (UnsupportedOperationException ignored) { + } + + int mode = calculateMode(basic, dos, p); + + return new StatResult( + 0, // dev + basic.fileKey() != null ? + basic.fileKey().hashCode() : 0, // ino (simulated) + mode, // mode + 1, // nlink + CURRENT_UID, // uid + CURRENT_GID, // gid + 0, // rdev + basic.size(), // size + basic.lastAccessTime().toMillis() / 1000, // atime + basic.lastModifiedTime().toMillis() / 1000, // mtime + basic.creationTime().toMillis() / 1000, // ctime + 4096, // blksize + (basic.size() + 511) / 512 // blocks + ); + } catch (Exception e) { + setErrno(getErrnoForException(e)); + return null; + } + } + + @Override + public StatResult lstat(String path) { + // Windows doesn't have symlink distinction in the same way + return stat(path); + } + + @Override + public int chmod(String path, int mode) { + // Windows only supports read-only attribute + try { + Path p = Path.of(path); + boolean readOnly = (mode & 0200) == 0; // No owner write = read-only + Files.setAttribute(p, "dos:readonly", readOnly); + return 0; + } catch (Exception e) { + setErrno(getErrnoForException(e)); + return -1; + } + } + + @Override + public int link(String oldPath, String newPath) { + try { + Files.createLink(Path.of(newPath), Path.of(oldPath)); + return 0; + } catch (Exception e) { + setErrno(getErrnoForException(e)); + return -1; + } + } + + @Override + public int utimes(String path, long atime, long mtime) { + try { + Path p = Path.of(path); + Files.setLastModifiedTime(p, java.nio.file.attribute.FileTime.fromMillis(mtime * 1000)); + // Access time not easily settable on Windows via NIO + return 0; + } catch (Exception e) { + setErrno(getErrnoForException(e)); + return -1; + } + } + + // ==================== Terminal Functions ==================== + + @Override + public int isatty(int fd) { + // Use Java Console detection + Console console = System.console(); + if (console != null) { + // If Console exists, stdin/stdout/stderr are likely terminals + return (fd >= 0 && fd <= 2) ? 1 : 0; + } + return 0; + } + + // ==================== File Control Functions ==================== + + @Override + public int fcntl(int fd, int cmd, int arg) { + // Not supported on Windows + setErrno(38); // ENOSYS + return -1; + } + + @Override + public int umask(int mask) { + // Simulated - Windows doesn't have umask + // Just return a reasonable default + return 022; + } + + // ==================== Error Handling ==================== + + @Override + public int errno() { + return threadErrno.get(); + } + + @Override + public void setErrno(int errno) { + threadErrno.set(errno); + } + + @Override + public String strerror(int errno) { + return switch (errno) { + case 0 -> "Success"; + case 1 -> "Operation not permitted"; + case 2 -> "No such file or directory"; + case 3 -> "No such process"; + case 5 -> "I/O error"; + case 10 -> "No child processes"; + case 13 -> "Permission denied"; + case 17 -> "File exists"; + case 22 -> "Invalid argument"; + case 38 -> "Function not implemented"; + default -> "Unknown error " + errno; + }; + } + + // ==================== Helper Methods ==================== + + /** + * Calculate Unix-style mode bits from Windows attributes. + */ + private int calculateMode(BasicFileAttributes basic, DosFileAttributes dos, Path path) { + int mode = 0; + + // File type + if (basic.isDirectory()) { + mode |= 0040000; // S_IFDIR + } else if (basic.isRegularFile()) { + mode |= 0100000; // S_IFREG + } else if (basic.isSymbolicLink()) { + mode |= 0120000; // S_IFLNK + } + + // Default permissions (Windows doesn't have Unix permissions) + // Check if read-only + boolean readOnly = dos != null && dos.isReadOnly(); + + if (basic.isDirectory()) { + mode |= readOnly ? 0555 : 0755; + } else { + mode |= readOnly ? 0444 : 0644; + + // Check if executable (by extension) + String name = path.getFileName().toString().toLowerCase(); + if (name.endsWith(".exe") || name.endsWith(".bat") || + name.endsWith(".cmd") || name.endsWith(".com")) { + mode |= 0111; + } + } + + return mode; + } + + /** + * Map Java exceptions to errno values. + */ + private int getErrnoForException(Exception e) { + String msg = e.getMessage(); + if (msg == null) return 5; // EIO + msg = msg.toLowerCase(); + + if (msg.contains("no such file") || msg.contains("cannot find")) return 2; + if (msg.contains("access") && msg.contains("denied")) return 13; + if (msg.contains("already exists")) return 17; + + return 5; // EIO + } +} diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index cd4ff091e..9de0132f4 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -4,7 +4,7 @@ import org.perlonjava.runtime.io.CustomFileChannel; import org.perlonjava.runtime.io.IOHandle; import org.perlonjava.runtime.io.LayeredIOHandle; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.perlmodule.Warnings; import org.perlonjava.runtime.runtimetypes.*; @@ -286,7 +286,7 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) } if (fd >= 0) { try { - boolean isTty = PosixLibrary.INSTANCE.isatty(fd) != 0; + boolean isTty = FFMPosix.get().isatty(fd) != 0; getGlobalVariable("main::!").set(0); return getScalarBoolean(isTty); } catch (Exception e) { diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index e6f949603..ae7831f1e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -6,7 +6,7 @@ import org.perlonjava.runtime.ForkOpenState; import org.perlonjava.runtime.io.*; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.runtimetypes.*; import java.io.File; @@ -1762,13 +1762,12 @@ public static RuntimeScalar fcntl(int ctx, RuntimeBase... args) { RuntimeScalar filenoResult = fh.ioHandle.fileno(); int fd = filenoResult.getDefinedBoolean() ? filenoResult.getInt() : -1; - // If we have a valid native fd, use jnr-posix + // If we have a valid native fd, use FFM POSIX if (fd >= 0 && !NativeUtils.IS_WINDOWS) { try { - jnr.constants.platform.Fcntl fcntlCmd = jnr.constants.platform.Fcntl.valueOf(function); - int result = PosixLibrary.INSTANCE.fcntl(fd, fcntlCmd, arg); + int result = FFMPosix.get().fcntl(fd, function, arg); if (result == -1) { - getGlobalVariable("main::!").set(PosixLibrary.INSTANCE.errno()); + getGlobalVariable("main::!").set(FFMPosix.get().errno()); return scalarUndef; } return new RuntimeScalar(result); diff --git a/src/main/java/org/perlonjava/runtime/operators/KillOperator.java b/src/main/java/org/perlonjava/runtime/operators/KillOperator.java index 55e03d091..10ae32f48 100644 --- a/src/main/java/org/perlonjava/runtime/operators/KillOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/KillOperator.java @@ -1,7 +1,8 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; +import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; import org.perlonjava.runtime.runtimetypes.PerlSignalQueue; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -14,6 +15,9 @@ */ public class KillOperator { + // FFM POSIX implementation + private static final FFMPosixInterface posix = FFMPosix.get(); + /** * Send a signal to a process (following Perl's kill operator) * @@ -153,9 +157,9 @@ private static boolean sendSignalToPid(int pid, int signal) { return false; } } else { - int result = PosixLibrary.INSTANCE.kill(pid, signal); + int result = posix.kill(pid, signal); if (result != 0) { - setErrno(PosixLibrary.INSTANCE.errno()); + setErrno(posix.errno()); return false; } return true; @@ -164,9 +168,9 @@ private static boolean sendSignalToPid(int pid, int signal) { // Helper method for sending signals to process groups (Unix only) private static boolean sendSignalToProcessGroup(int pgid, int signal) { - int result = PosixLibrary.INSTANCE.kill(-pgid, signal); + int result = posix.kill(-pgid, signal); if (result != 0) { - setErrno(PosixLibrary.INSTANCE.errno()); + setErrno(posix.errno()); return false; } return true; diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index 6c50ce746..c8610205e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -1,7 +1,7 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.regex.RegexTimeoutCharSequence; import org.perlonjava.runtime.regex.RegexTimeoutException; import org.perlonjava.runtime.regex.RuntimeRegex; @@ -64,7 +64,7 @@ public static RuntimeScalar chmod(RuntimeList runtimeList) { success = false; } } else { - int result = PosixLibrary.INSTANCE.chmod(path, mode); + int result = FFMPosix.get().chmod(path, mode); success = (result == 0); } diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 8371b6bc4..7e4f50e3a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -1,12 +1,12 @@ package org.perlonjava.runtime.operators; -import jnr.posix.FileStat; import org.perlonjava.runtime.io.ClosedIOHandle; import org.perlonjava.runtime.io.CustomFileChannel; import org.perlonjava.runtime.io.IOHandle; import org.perlonjava.runtime.io.LayeredIOHandle; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; +import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; import org.perlonjava.runtime.runtimetypes.*; import java.io.IOException; @@ -27,19 +27,22 @@ public class Stat { static NativeStatFields lastNativeStatFields; + + // FFM POSIX implementation + private static final FFMPosixInterface posix = FFMPosix.get(); static NativeStatFields nativeStat(String path, boolean followLinks) { try { if (NativeUtils.IS_WINDOWS) return null; - FileStat fs = followLinks - ? PosixLibrary.INSTANCE.stat(path) - : PosixLibrary.INSTANCE.lstat(path); - if (fs == null) return null; + FFMPosixInterface.StatResult sr = followLinks + ? posix.stat(path) + : posix.lstat(path); + if (sr == null) return null; return new NativeStatFields( - fs.dev(), fs.ino(), fs.mode(), fs.nlink(), - fs.uid(), fs.gid(), fs.rdev(), fs.st_size(), - fs.atime(), fs.mtime(), fs.ctime(), - fs.blockSize(), fs.blocks() + sr.dev(), sr.ino(), sr.mode(), sr.nlink(), + sr.uid(), sr.gid(), sr.rdev(), sr.size(), + sr.atime(), sr.mtime(), sr.ctime(), + sr.blksize(), sr.blocks() ); } catch (Throwable e) { return null; diff --git a/src/main/java/org/perlonjava/runtime/operators/UmaskOperator.java b/src/main/java/org/perlonjava/runtime/operators/UmaskOperator.java index 865fd8e33..cb0c7914c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/UmaskOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/UmaskOperator.java @@ -1,7 +1,7 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeList; @@ -69,18 +69,18 @@ private static RuntimeScalar umaskPosix(RuntimeList args) { // No argument - just get current umask // umask() always sets a value, so we need to call it twice // to get the current value without changing it - int current = PosixLibrary.INSTANCE.umask(0); - PosixLibrary.INSTANCE.umask(current); // Restore original + int current = FFMPosix.get().umask(0); + FFMPosix.get().umask(current); // Restore original return new RuntimeScalar(current); } // Set new umask and get previous value - int previousMask = PosixLibrary.INSTANCE.umask(newMask); + int previousMask = FFMPosix.get().umask(newMask); // Check Perl's special behavior: die if trying to restrict self if ((newMask & 0700) > 0) { // Restore previous umask before throwing - PosixLibrary.INSTANCE.umask(previousMask); + FFMPosix.get().umask(previousMask); throw new PerlCompilerException("umask not implemented"); } @@ -157,8 +157,8 @@ public static int getCurrentUmask() { } else { try { // Get current umask by setting and immediately restoring - int current = PosixLibrary.INSTANCE.umask(0); - PosixLibrary.INSTANCE.umask(current); + int current = FFMPosix.get().umask(0); + FFMPosix.get().umask(current); return current; } catch (Exception e) { // If native call fails, return default @@ -215,7 +215,7 @@ public static void resetToDefault() { windowsSimulatedUmask = DEFAULT_UMASK; } else { try { - PosixLibrary.INSTANCE.umask(DEFAULT_UMASK); + FFMPosix.get().umask(DEFAULT_UMASK); } catch (Exception ignored) { // Best effort } diff --git a/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java b/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java index 5d1db08ef..2288d11ba 100644 --- a/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java @@ -1,7 +1,7 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -77,9 +77,7 @@ private static boolean changeFileTimes(RuntimeScalar fileArg, long accessTime, l private static boolean changeFileTimesPosix(String filename, long accessTime, long modTime) { try { - long[] atimeval = {accessTime, 0}; - long[] mtimeval = {modTime, 0}; - int result = PosixLibrary.INSTANCE.utimes(filename, atimeval, mtimeval); + int result = FFMPosix.get().utimes(filename, accessTime, modTime); if (result == 0) return true; return changeFileTimesJava(filename, accessTime, modTime); } catch (Exception e) { diff --git a/src/main/java/org/perlonjava/runtime/operators/WaitpidOperator.java b/src/main/java/org/perlonjava/runtime/operators/WaitpidOperator.java index 14d0c8501..8cb7a6dfc 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WaitpidOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/WaitpidOperator.java @@ -1,7 +1,7 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeIO; @@ -49,7 +49,7 @@ private static RuntimeScalar waitpidPosix(int pid, int flags) { } try { int[] status = new int[1]; - long result = PosixLibrary.INSTANCE.waitpid(pid, status, flags); + long result = FFMPosix.get().waitpid(pid, status, flags); if (result > 0) { setExitStatus(status[0]); @@ -57,7 +57,7 @@ private static RuntimeScalar waitpidPosix(int pid, int flags) { } else if (result == 0) { return new RuntimeScalar(0); } else { - int errno = PosixLibrary.INSTANCE.errno(); + int errno = FFMPosix.get().errno(); if (errno == 10) { // ECHILD return new RuntimeScalar(-1); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java index 2c096c986..eee4048ff 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java @@ -1,7 +1,7 @@ package org.perlonjava.runtime.perlmodule; import org.perlonjava.runtime.nativ.NativeUtils; -import org.perlonjava.runtime.nativ.PosixLibrary; +import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.operators.Time; import org.perlonjava.runtime.runtimetypes.*; @@ -321,7 +321,7 @@ public static RuntimeList strerror(RuntimeArray args, int ctx) { // Return a basic error message - could be enhanced with actual errno mapping String msg = "Error " + errno; try { - msg = org.perlonjava.runtime.nativ.PosixLibrary.INSTANCE.strerror(errno); + msg = FFMPosix.get().strerror(errno); } catch (Exception e) { // Fall back to generic message } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 1a45e0be8..a8804c977 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -92,8 +92,8 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.globalVariables.put(taintVarName, compilerOptions.taintMode ? RuntimeScalarCache.scalarOne : RuntimeScalarCache.scalarZero); } - GlobalVariable.getGlobalVariable("main::>"); // TODO - GlobalVariable.getGlobalVariable("main::<"); // TODO + GlobalVariable.globalVariables.put("main::>", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_UID)); // $> - effective UID (lazy) + GlobalVariable.globalVariables.put("main::<", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_UID)); // $< - real UID (lazy) GlobalVariable.getGlobalVariable("main::;").set("\034"); // initialize $; (SUBSEP) to \034 GlobalVariable.globalVariables.put("main::(", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_GID)); // $( - real GID (lazy) GlobalVariable.globalVariables.put("main::)", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_GID)); // $) - effective GID (lazy) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index a202abed8..71f98768e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -227,6 +227,14 @@ public RuntimeScalar getValueAsScalar() { // $) - Effective group ID (lazy evaluation to avoid JNA overhead at startup) yield new RuntimeScalar(NativeUtils.getegid(0)); } + case REAL_UID -> { + // $< - Real user ID (lazy evaluation to avoid JNA overhead at startup) + yield NativeUtils.getuid(0); + } + case EFFECTIVE_UID -> { + // $> - Effective user ID (lazy evaluation to avoid JNA overhead at startup) + yield NativeUtils.geteuid(0); + } }; return result; } catch (IllegalStateException e) { @@ -413,6 +421,8 @@ public enum Id { HINTS, // $^H - Compile-time hints (strict, etc.) REAL_GID, // $( - Real group ID (lazy, JNA call only on access) EFFECTIVE_GID, // $) - Effective group ID (lazy, JNA call only on access) + REAL_UID, // $< - Real user ID (lazy, JNA call only on access) + EFFECTIVE_UID, // $> - Effective user ID (lazy, JNA call only on access) } private record InputLineState(RuntimeIO lastHandle, int lastLineNumber, RuntimeScalar localValue) {