|
| 1 | +# Port CPAN Module to PerlOnJava |
| 2 | + |
| 3 | +This skill guides you through porting a CPAN module with XS/C components to PerlOnJava using Java implementations. |
| 4 | + |
| 5 | +## When to Use This Skill |
| 6 | + |
| 7 | +- User asks to add a CPAN module to PerlOnJava |
| 8 | +- User asks to port a Perl module with XS code |
| 9 | +- User wants to implement Perl module functionality in Java |
| 10 | + |
| 11 | +## Key Principles |
| 12 | + |
| 13 | +1. **Reuse as much original code as possible** - Most CPAN modules are 70-90% pure Perl. Only the XS/C portions need Java replacements. Copy the original `.pm` code and adapt minimally. |
| 14 | + |
| 15 | +2. **Always inspect the XS source** - The `.xs` file reveals exactly what needs Java implementation. Study it to understand the C algorithms, edge cases, and expected behavior. |
| 16 | + |
| 17 | +3. **Credit original authors** - Always preserve the original AUTHORS and COPYRIGHT sections in the POD. Add a note that this is a PerlOnJava port. |
| 18 | + |
| 19 | +## Overview |
| 20 | + |
| 21 | +PerlOnJava supports three types of modules: |
| 22 | +1. **Pure Perl modules** - Work directly, no Java needed |
| 23 | +2. **Java-implemented modules (XSLoader)** - Replace XS/C with Java |
| 24 | +3. **Built-in modules (GlobalContext)** - Internal only |
| 25 | + |
| 26 | +**Most CPAN ports use type #2 (XSLoader).** |
| 27 | + |
| 28 | +## Step-by-Step Process |
| 29 | + |
| 30 | +### Phase 1: Analysis |
| 31 | + |
| 32 | +1. **Fetch the original module source:** |
| 33 | + ``` |
| 34 | + https://fastapi.metacpan.org/v1/source/AUTHOR/Module-Version/Module.pm |
| 35 | + https://fastapi.metacpan.org/v1/source/AUTHOR/Module-Version/Module.xs |
| 36 | + ``` |
| 37 | + |
| 38 | +2. **Study the XS file thoroughly:** |
| 39 | + - Look for `MODULE = ` and `PACKAGE = ` declarations |
| 40 | + - Identify each XS function (appears after `void` or return type) |
| 41 | + - Read the C code to understand algorithms and edge cases |
| 42 | + - Note any platform-specific code (WIN32, etc.) |
| 43 | + - Check for copyright notices to preserve |
| 44 | + |
| 45 | +3. **Identify what needs Java implementation:** |
| 46 | + - Functions defined in `.xs` files |
| 47 | + - Functions that call C libraries (strftime, crypt, etc.) |
| 48 | + - Functions loaded via `XSLoader::load()` |
| 49 | + |
| 50 | +4. **Identify what can be reused as pure Perl (typically 70-90%):** |
| 51 | + - Most accessor methods |
| 52 | + - Helper/utility functions |
| 53 | + - Overloaded operators |
| 54 | + - Import/export logic |
| 55 | + - Format translation maps |
| 56 | + - Constants and configuration |
| 57 | + |
| 58 | +5. **Check for dependencies:** |
| 59 | + - Other modules the target depends on |
| 60 | + - Whether those dependencies exist in PerlOnJava |
| 61 | + |
| 62 | +6. **Check available Java libraries:** |
| 63 | + - Review `pom.xml` and `build.gradle` for already-imported dependencies |
| 64 | + - Common libraries already available: Gson, jnr-posix, jnr-ffi, SnakeYAML, etc. |
| 65 | + - Consider if a Java library can replace the XS functionality directly |
| 66 | + |
| 67 | +7. **Check existing PerlOnJava infrastructure:** |
| 68 | + - `org.perlonjava.runtime.nativ.PosixLibrary` - JNR-POSIX wrapper for native calls |
| 69 | + - `org.perlonjava.runtime.nativ.NativeUtils` - Cross-platform utilities with Windows fallbacks |
| 70 | + - `org.perlonjava.runtime.operators.*` - Existing operator implementations |
| 71 | + |
| 72 | +### Phase 2: Create Java Implementation |
| 73 | + |
| 74 | +**File location:** `src/main/java/org/perlonjava/runtime/perlmodule/` |
| 75 | + |
| 76 | +**Naming convention:** `Module::Name` → `ModuleName.java` |
| 77 | +- `Time::Piece` → `TimePiece.java` |
| 78 | +- `Digest::MD5` → `DigestMD5.java` |
| 79 | +- `DBI` → `DBI.java` |
| 80 | + |
| 81 | +**Basic structure:** |
| 82 | +```java |
| 83 | +package org.perlonjava.runtime.perlmodule; |
| 84 | + |
| 85 | +import org.perlonjava.runtime.runtimetypes.*; |
| 86 | + |
| 87 | +public class ModuleName extends PerlModuleBase { |
| 88 | + |
| 89 | + public ModuleName() { |
| 90 | + super("Module::Name", false); // false = not a pragma |
| 91 | + } |
| 92 | + |
| 93 | + public static void initialize() { |
| 94 | + ModuleName module = new ModuleName(); |
| 95 | + try { |
| 96 | + // Register methods - Perl name, Java method name (null = same), prototype |
| 97 | + module.registerMethod("xs_function", null); |
| 98 | + module.registerMethod("perl_name", "javaMethodName", null); |
| 99 | + } catch (NoSuchMethodException e) { |
| 100 | + System.err.println("Warning: Missing method: " + e.getMessage()); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Method signature: (RuntimeArray args, int ctx) -> RuntimeList |
| 105 | + public static RuntimeList xs_function(RuntimeArray args, int ctx) { |
| 106 | + // args.get(0) = first argument ($self for methods) |
| 107 | + // ctx = RuntimeContextType.SCALAR, LIST, or VOID |
| 108 | + |
| 109 | + String param = args.get(0).toString(); |
| 110 | + int number = args.get(1).getInt(); |
| 111 | + |
| 112 | + // Return value |
| 113 | + return new RuntimeScalar(result).getList(); |
| 114 | + } |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +### Phase 3: Create Perl Wrapper |
| 119 | + |
| 120 | +**File location:** `src/main/perl/lib/Module/Name.pm` |
| 121 | + |
| 122 | +**Template:** |
| 123 | +```perl |
| 124 | +package Module::Name; |
| 125 | + |
| 126 | +use strict; |
| 127 | +use warnings; |
| 128 | + |
| 129 | +our $VERSION = '1.00'; |
| 130 | + |
| 131 | +# Load Java implementation |
| 132 | +use XSLoader; |
| 133 | +XSLoader::load('Module::Name', $VERSION); |
| 134 | + |
| 135 | +# Pure Perl code from original module goes here |
| 136 | +# (accessors, helpers, overloads, etc.) |
| 137 | + |
| 138 | +1; |
| 139 | + |
| 140 | +__END__ |
| 141 | +
|
| 142 | +=head1 NAME |
| 143 | +
|
| 144 | +Module::Name - Description |
| 145 | +
|
| 146 | +=head1 DESCRIPTION |
| 147 | +
|
| 148 | +This is a port of the CPAN Module::Name module for PerlOnJava. |
| 149 | +
|
| 150 | +=head1 AUTHOR |
| 151 | +
|
| 152 | +Original Author Name, original@email.com |
| 153 | +
|
| 154 | +Additional Author, other@email.com (if applicable) |
| 155 | +
|
| 156 | +=head1 COPYRIGHT AND LICENSE |
| 157 | +
|
| 158 | +Copyright YEAR, Original Copyright Holder. |
| 159 | +
|
| 160 | +This module is free software; you may distribute it under the same terms |
| 161 | +as Perl itself. |
| 162 | +
|
| 163 | +=cut |
| 164 | +``` |
| 165 | +
|
| 166 | +### Phase 4: Testing |
| 167 | +
|
| 168 | +1. **Create test file:** `src/test/resources/module_name.t` |
| 169 | +
|
| 170 | +2. **Compare with system Perl:** |
| 171 | + ```bash |
| 172 | + # Create test script |
| 173 | + cat > /tmp/test.pl << 'EOF' |
| 174 | + use Module::Name; |
| 175 | + # test code |
| 176 | + EOF |
| 177 | + |
| 178 | + # Run with both |
| 179 | + perl /tmp/test.pl |
| 180 | + ./jperl /tmp/test.pl |
| 181 | + ``` |
| 182 | +
|
| 183 | +3. **Build and verify:** |
| 184 | + ```bash |
| 185 | + ./gradlew build -x test |
| 186 | + ./jperl -e 'use Module::Name; ...' |
| 187 | + ``` |
| 188 | +
|
| 189 | +## Common Patterns |
| 190 | +
|
| 191 | +### Reading XS Files |
| 192 | +
|
| 193 | +XS files have a specific structure: |
| 194 | +
|
| 195 | +```c |
| 196 | +MODULE = Time::Piece PACKAGE = Time::Piece |
| 197 | +
|
| 198 | +void |
| 199 | +_strftime(fmt, epoch, islocal = 1) |
| 200 | + char * fmt |
| 201 | + time_t epoch |
| 202 | + int islocal |
| 203 | +CODE: |
| 204 | + /* C implementation here */ |
| 205 | + ST(0) = sv_2mortal(newSVpv(result, len)); |
| 206 | +``` |
| 207 | +
|
| 208 | +Key elements to identify: |
| 209 | +- **Function name**: `_strftime` (usually prefixed with `_` for internal XS) |
| 210 | +- **Parameters**: `fmt`, `epoch`, `islocal` with their C types |
| 211 | +- **Default values**: `islocal = 1` |
| 212 | +- **Return mechanism**: `ST(0)`, `RETVAL`, or stack manipulation |
| 213 | +
|
| 214 | +### Converting XS to Java |
| 215 | +
|
| 216 | +| XS Pattern | Java Equivalent | |
| 217 | +|------------|-----------------| |
| 218 | +| `SvIV(arg)` | `args.get(i).getInt()` | |
| 219 | +| `SvNV(arg)` | `args.get(i).getDouble()` | |
| 220 | +| `SvPV(arg, len)` | `args.get(i).toString()` | |
| 221 | +| `newSViv(n)` | `new RuntimeScalar(n)` | |
| 222 | +| `newSVnv(n)` | `new RuntimeScalar(n)` | |
| 223 | +| `newSVpv(s, len)` | `new RuntimeScalar(s)` | |
| 224 | +| `av_fetch(av, i, 0)` | `array.get(i)` | |
| 225 | +| `hv_fetch(hv, k, len, 0)` | `hash.get(k)` | |
| 226 | +| `RETVAL` / `ST(0)` | `return new RuntimeScalar(x).getList()` | |
| 227 | +
|
| 228 | +### Using Existing Java Libraries |
| 229 | +
|
| 230 | +**Check `build.gradle` for available dependencies:** |
| 231 | +```bash |
| 232 | +grep "implementation" build.gradle |
| 233 | +``` |
| 234 | +
|
| 235 | +**Common libraries already in PerlOnJava:** |
| 236 | +
|
| 237 | +| Java Library | Use Case | Example Module | |
| 238 | +|--------------|----------|----------------| |
| 239 | +| Gson | JSON parsing/encoding | `Json.java` | |
| 240 | +| jnr-posix | Native POSIX calls | `POSIX.java` | |
| 241 | +| jnr-ffi | Foreign function interface | Native bindings | |
| 242 | +| SnakeYAML | YAML parsing | `YAMLPP.java` | |
| 243 | +| TOML4J | TOML parsing | `Toml.java` | |
| 244 | +| Java stdlib | Crypto, encoding, time | Various | |
| 245 | +
|
| 246 | +**Example: JSON.java uses Gson directly:** |
| 247 | +```java |
| 248 | +import com.google.gson.Gson; |
| 249 | +import com.google.gson.GsonBuilder; |
| 250 | +
|
| 251 | +public static RuntimeList encode_json(RuntimeArray args, int ctx) { |
| 252 | + Gson gson = new GsonBuilder().create(); |
| 253 | + String json = gson.toJson(convertToJava(args.get(0))); |
| 254 | + return new RuntimeScalar(json).getList(); |
| 255 | +} |
| 256 | +``` |
| 257 | +
|
| 258 | +**Standard Java imports:** |
| 259 | +```java |
| 260 | +// Time operations |
| 261 | +import java.time.*; |
| 262 | +import java.time.format.DateTimeFormatter; |
| 263 | +
|
| 264 | +// Crypto |
| 265 | +import java.security.MessageDigest; |
| 266 | +
|
| 267 | +// Encoding |
| 268 | +import java.util.Base64; |
| 269 | +import java.nio.charset.StandardCharsets; |
| 270 | +
|
| 271 | +// Native POSIX calls (with Windows fallbacks) |
| 272 | +import org.perlonjava.runtime.nativ.PosixLibrary; |
| 273 | +import org.perlonjava.runtime.nativ.NativeUtils; |
| 274 | +``` |
| 275 | +
|
| 276 | +**Using PosixLibrary for native calls:** |
| 277 | +```java |
| 278 | +// Direct POSIX call (Unix only) |
| 279 | +int uid = PosixLibrary.INSTANCE.getuid(); |
| 280 | +
|
| 281 | +// Cross-platform with Windows fallback (preferred) |
| 282 | +RuntimeScalar uid = NativeUtils.getuid(ctx); |
| 283 | +``` |
| 284 | +
|
| 285 | +### Returning Different Types |
| 286 | +
|
| 287 | +```java |
| 288 | +// Scalar |
| 289 | +return new RuntimeScalar(value).getList(); |
| 290 | +
|
| 291 | +// List |
| 292 | +RuntimeList result = new RuntimeList(); |
| 293 | +result.add(new RuntimeScalar(item1)); |
| 294 | +result.add(new RuntimeScalar(item2)); |
| 295 | +return result; |
| 296 | +
|
| 297 | +// Array reference |
| 298 | +RuntimeArray arr = new RuntimeArray(); |
| 299 | +arr.push(new RuntimeScalar(item)); |
| 300 | +return arr.createReference().getList(); |
| 301 | +
|
| 302 | +// Hash reference |
| 303 | +RuntimeHash hash = new RuntimeHash(); |
| 304 | +hash.put("key", new RuntimeScalar(value)); |
| 305 | +return hash.createReference().getList(); |
| 306 | +``` |
| 307 | +
|
| 308 | +### Handling Context |
| 309 | +
|
| 310 | +```java |
| 311 | +public static RuntimeList myMethod(RuntimeArray args, int ctx) { |
| 312 | + if (ctx == RuntimeContextType.SCALAR) { |
| 313 | + // Return single value |
| 314 | + return new RuntimeScalar(count).getList(); |
| 315 | + } else { |
| 316 | + // Return list |
| 317 | + RuntimeList result = new RuntimeList(); |
| 318 | + for (String item : items) { |
| 319 | + result.add(new RuntimeScalar(item)); |
| 320 | + } |
| 321 | + return result; |
| 322 | + } |
| 323 | +} |
| 324 | +``` |
| 325 | +
|
| 326 | +## Checklist |
| 327 | +
|
| 328 | +### Pre-porting |
| 329 | +- [ ] Fetch original `.pm` and `.xs` source |
| 330 | +- [ ] Study XS code to understand C algorithms and edge cases |
| 331 | +- [ ] Identify XS functions that need Java implementation |
| 332 | +- [ ] Check dependencies exist in PerlOnJava |
| 333 | +- [ ] Check `build.gradle`/`pom.xml` for usable Java libraries |
| 334 | +- [ ] Check `nativ/` package for POSIX functionality |
| 335 | +- [ ] Review existing similar modules for patterns |
| 336 | +
|
| 337 | +### Implementation |
| 338 | +- [ ] Create `ModuleName.java` with XS replacements |
| 339 | +- [ ] Create `Module/Name.pm` with pure Perl code |
| 340 | +- [ ] Add proper author/copyright attribution |
| 341 | +- [ ] Register all methods in `initialize()` |
| 342 | +
|
| 343 | +### Testing |
| 344 | +- [ ] Build compiles without errors: `./gradlew build -x test` |
| 345 | +- [ ] Basic functionality works: `./jperl -e 'use Module::Name; ...'` |
| 346 | +- [ ] Compare output with system Perl |
| 347 | +- [ ] Test edge cases identified in XS code |
| 348 | +
|
| 349 | +### Documentation |
| 350 | +- [ ] Add POD with AUTHOR and COPYRIGHT sections |
| 351 | +- [ ] Credit original authors |
| 352 | +
|
| 353 | +## Example: Time::Piece Port |
| 354 | +
|
| 355 | +**Files created:** |
| 356 | +- `src/main/java/org/perlonjava/runtime/perlmodule/TimePiece.java` |
| 357 | +- `src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java` (for strftime) |
| 358 | +- `src/main/perl/lib/Time/Piece.pm` |
| 359 | +- `src/main/perl/lib/Time/Seconds.pm` |
| 360 | +
|
| 361 | +**XS functions replaced:** |
| 362 | +| XS Function | Java Implementation | |
| 363 | +|-------------|---------------------| |
| 364 | +| `_strftime(fmt, epoch, islocal)` | `DateTimeFormatter` with format mapping | |
| 365 | +| `_strptime(str, fmt, gmt, locale)` | `DateTimeFormatter.parse()` | |
| 366 | +| `_tzset()` | No-op (Java handles TZ) | |
| 367 | +| `_crt_localtime(epoch)` | `ZonedDateTime` conversion | |
| 368 | +| `_crt_gmtime(epoch)` | `ZonedDateTime` at UTC | |
| 369 | +| `_get_localization()` | `DateFormatSymbols` | |
| 370 | +| `_mini_mktime(...)` | `LocalDateTime` normalization | |
| 371 | +
|
| 372 | +**Pure Perl reused (~80%):** |
| 373 | +- All accessor methods (sec, min, hour, year, etc.) |
| 374 | +- Formatting helpers (ymd, hms, datetime) |
| 375 | +- Julian day calculations |
| 376 | +- Overloaded operators |
| 377 | +- Import/export logic |
| 378 | +
|
| 379 | +## Troubleshooting |
| 380 | +
|
| 381 | +### "Can't load Java XS module" |
| 382 | +- Check class name matches: `Module::Name` → `ModuleName.java` |
| 383 | +- Verify `initialize()` method exists and is static |
| 384 | +- Check package is `org.perlonjava.runtime.perlmodule` |
| 385 | +
|
| 386 | +### Method not found |
| 387 | +- Ensure method is registered in `initialize()` |
| 388 | +- Check method signature: `public static RuntimeList name(RuntimeArray args, int ctx)` |
| 389 | +
|
| 390 | +### Different output than system Perl |
| 391 | +- Compare with fixed test values (not current time) |
| 392 | +- Check locale handling |
| 393 | +- Verify edge cases from XS comments |
| 394 | +
|
| 395 | +## References |
| 396 | +
|
| 397 | +- Module porting guide: `docs/guides/module-porting.md` |
| 398 | +- Existing modules: `src/main/java/org/perlonjava/runtime/perlmodule/` |
| 399 | +- Runtime types: `src/main/java/org/perlonjava/runtime/runtimetypes/` |
0 commit comments