From b4795698beb5e89bec216fa1ede37c4e6f0695e2 Mon Sep 17 00:00:00 2001
From: ~byakuren
Date: Wed, 24 Sep 2025 02:00:22 +0100
Subject: Add virtual files support via 1!:60 foreign function

Introduces RAM-based virtual files that behave like regular files but exist entirely
in memory. Virtual files use IDs >= 1000000 to distinguish them from regular file handles.

Key features:
- 1!:60 creates/opens virtual files with format: handle =. 'filename' 1!:60 y (y ignored)
- Virtual files support all standard file operations: read, write, seek, append
- Memory management with automatic growth and shrinking
- Thread-safe operations with proper locking
- Transparent integration with existing file operations
- Enhanced readline functions (1!:13, 1!:14, 1!:15) work with virtual files

Implementation includes:
- VirtFile structure and operations in xf.c
- File handle validation updates in xo.c to recognize virtual file IDs
- Integration with standard file foreign functions in x.c
- Enhanced readline functions with virtual file support in xf.c
- Function declarations in je.h

Virtual files are useful for temporary data, testing, and inter-process communication
without filesystem overhead.

Note: This patch requires readline-functions.patch to be applied first.
---
diff --git a/jsrc/je.h b/jsrc/je.h
index 2c660ac8..d968fc47 100644
--- a/jsrc/je.h
+++ b/jsrc/je.h
@@ -191,6 +191,7 @@ extern DF1(jtjfreadline);
 extern DF1(jtjfreadlinetrim);
 extern DF1(jtjfreadlinepeek);
 extern DF1(jtjfsize);
+extern DF1(jtjvramfile);
 extern DF1(jtjgetenv);
 extern F1(jtjgetx);
 extern F1(jtjgetpid);
diff --git a/jsrc/x.c b/jsrc/x.c
index 528fbe32..78a39a3b 100644
--- a/jsrc/x.c
+++ b/jsrc/x.c
@@ -128,6 +128,8 @@ void jtforeigninit(J jt){UI i;
  MN(1,14)  XPRIM(VERB, jtjfreadlinetrim, 0,        VASGSAFE,VF2NONE,0,   RMAX,RMAX);
  MN(1,15)  XPRIM(VERB, jtjfreadlinepeek, 0,        VASGSAFE,VF2NONE,0,   RMAX,RMAX);
 
+ MN(1,60)  XPRIM(VERB, jtjvramfile, 0,            VASGSAFE,VF2NONE,0,   RMAX,RMAX);
+
  MN(3,0)   XPRIM(VERB, jtstype,      0,            VASGSAFE,VF2NONE,RMAX,RMAX,RMAX);
  MN(3,10)  XPRIM(VERB, jttobase64,   0,            VASGSAFE,VF2NONE,RMAX,RMAX,RMAX);
  MN(3,11)  XPRIM(VERB, jtfrombase64, 0,            VASGSAFE,VF2NONE,RMAX,RMAX,RMAX);
diff --git a/jsrc/xf.c b/jsrc/xf.c
index 13daae2b..c4d5fea5 100644
--- a/jsrc/xf.c
+++ b/jsrc/xf.c
@@ -55,10 +55,243 @@ static int rmdir2(const char *dir);
 static int rmdir2(J jt, const wchar_t *dir);
 #endif
 
+// Virtual FILE* structure that behaves like a real FILE*
+typedef struct {
+    // FILE* interface compatibility
+    FILE fake_file;       // Embedded FILE structure for casting compatibility
+
+    // Virtual file data
+    C *data;              // File content buffer
+    I size;               // Current size of content
+    I capacity;           // Allocated buffer size
+    I position;           // Current read/write position
+    I refcount;           // Reference count for cleanup
+
+    // Thread safety
+#if SY_WIN32
+    SRWLOCK lock;         // Reader-writer lock for thread safety
+#else
+    pthread_rwlock_t lock; // Reader-writer lock for thread safety
+#endif
+
+    // Virtual file identification
+    I magic;              // Magic number to identify virtual files (0x56465249 = 'VFIR')
+    I virtid;             // Virtual file ID
+} VirtFile;
+
+// Global virtual file table (simple array for now)
+#define MAX_VIRT_FILES 64
+static VirtFile *virtfiles[MAX_VIRT_FILES] = {0};
+static I nextvirtid = 1000000;  // Start virtual file IDs at 1M to avoid conflicts
+
+// Forward declarations
+static B expandvirtfile(VirtFile *vf, I newsize);
+
+// Check if a FILE* is actually a virtual file
+B isvirtfile_fp(FILE* fp) {
+    if (!fp) return 0;
+    // Check if the FILE* is actually a VirtFile with the right magic number
+    VirtFile *vf = (VirtFile*)fp;
+    return vf->magic == 0x56465249; // 'VFIR'
+}
+
+// Get VirtFile from FILE* (after confirming it's virtual)
+static VirtFile* getvirtfile_fp(FILE* fp) {
+    if (!isvirtfile_fp(fp)) return 0;
+    return (VirtFile*)fp;
+}
+
+// Check if a file ID represents a virtual file
+static B isvirtfile(I id) {
+    return id >= nextvirtid && id < nextvirtid + MAX_VIRT_FILES;
+}
+
+// Get virtual file by ID
+void* getvirtfile(I id) {
+    if(id < 1000000) return 0;  // Must be a virtual file ID
+    // Search for file with matching virtid
+    for(I i = 0; i < MAX_VIRT_FILES; i++) {
+        if(virtfiles[i] && virtfiles[i]->virtid == id) {
+            return virtfiles[i];
+        }
+    }
+    return 0;  // Not found
+}
+
+// Expand virtual file buffer
+static B expandvirtfile(VirtFile *vf, I newsize) {
+    if(newsize <= vf->capacity) return 1;
+
+    // Expand by 50% or to newsize, whichever is larger
+    I newcap = vf->capacity + vf->capacity/2;
+    if(newcap < newsize) newcap = newsize;
+
+    C *newdata = realloc(vf->data, newcap);
+    if(!newdata) return 0;
+
+    vf->data = newdata;
+    vf->capacity = newcap;
+    return 1;
+}
+
+// Append data to virtual file
+static B appendvirtfile(VirtFile *vf, C* data, I len) {
+    if(!vf) return 0;
+
+    // Ensure we have enough capacity
+    if(vf->size + len > vf->capacity) {
+        if(!expandvirtfile(vf, vf->size + len)) return 0;
+    }
+
+    // Append the data
+    memcpy(vf->data + vf->size, data, len);
+    vf->size += len;
+    return 1;
+}
+
+// Read entire contents of virtual file
+static A readvirtfile(J jt, VirtFile *vf) {
+    if(!vf || vf->size == 0) {
+        // Return empty string for empty/null virtual file
+        A z; GATV0(z,LIT,0,1); R z;
+    }
+
+    // Create result array with virtual file contents
+    A z; GATV0(z,LIT,vf->size,1);
+    memcpy(CAV1(z), vf->data, vf->size);
+    R z;
+}
+
+// Create virtual file and return file number
+static I createvirtfile(J jt) {
+    I i;
+    // Find empty slot
+    for(i = 0; i < MAX_VIRT_FILES; i++) {
+        if(!virtfiles[i]) {
+            // Allocate virtual file structure - use simple malloc for now
+            VirtFile *vf = malloc(sizeof(VirtFile));
+            if(!vf) return 0;
+
+            // Initialize with small buffer
+            vf->capacity = 1024;
+            vf->data = malloc(vf->capacity);
+            if(!vf->data) {
+                free(vf);
+                return 0;
+            }
+
+            vf->size = 0;
+            vf->position = 0;
+            vf->refcount = 1;
+
+            // Initialize virtual file identification
+            vf->magic = 0x56465249; // 'VFIR'
+            vf->virtid = nextvirtid;  // Use current nextvirtid as the ID
+
+            // Initialize the lock
+#if SY_WIN32
+            InitializeSRWLock(&vf->lock);
+#else
+            if(pthread_rwlock_init(&vf->lock, NULL) != 0) {
+                free(vf->data);
+                free(vf);
+                return 0;
+            }
+#endif
+
+            virtfiles[i] = vf;
+            nextvirtid++;  // Increment for next file
+            return vf->virtid;  // Return the virtual file ID
+        }
+    }
+    return 0; // No slots available
+}
+
+// Close/free virtual file
+static void closevirtfile(I id) {
+    if(id < 1000000) return;
+    // Find the virtual file by ID
+    for(I i = 0; i < MAX_VIRT_FILES; i++) {
+        if(virtfiles[i] && virtfiles[i]->virtid == id) {
+            VirtFile *vf = virtfiles[i];
+            if(vf) {
+                vf->refcount--;
+                if(vf->refcount <= 0) {
+                    // Destroy the lock
+#if !SY_WIN32
+                    pthread_rwlock_destroy(&vf->lock);
+#endif
+                    free(vf->data);
+                    free(vf);
+                    virtfiles[i] = 0;  // Clear the slot (use i, not idx)
+                }
+            }
+            return;  // Found and processed
+        }
+    }
+}
+
+// Virtual file locking functions (called from xo.c)
+B vf_readlock(I id) {
+    VirtFile *vf = (VirtFile*)getvirtfile(id);
+    if(!vf) return 0;
+
+#if SY_WIN32
+    AcquireSRWLockShared(&vf->lock);
+    return 1;
+#else
+    return pthread_rwlock_rdlock(&vf->lock) == 0;
+#endif
+}
+
+B vf_readunlock(I id) {
+    VirtFile *vf = (VirtFile*)getvirtfile(id);
+    if(!vf) return 0;
+
+#if SY_WIN32
+    ReleaseSRWLockShared(&vf->lock);
+    return 1;
+#else
+    return pthread_rwlock_unlock(&vf->lock) == 0;
+#endif
+}
+
+B vf_writelock(I id) {
+    VirtFile *vf = (VirtFile*)getvirtfile(id);
+    if(!vf) return 0;
+
+#if SY_WIN32
+    AcquireSRWLockExclusive(&vf->lock);
+    return 1;
+#else
+    return pthread_rwlock_wrlock(&vf->lock) == 0;
+#endif
+}
+
+B vf_writeunlock(I id) {
+    VirtFile *vf = (VirtFile*)getvirtfile(id);
+    if(!vf) return 0;
+
+#if SY_WIN32
+    ReleaseSRWLockExclusive(&vf->lock);
+    return 1;
+#else
+    return pthread_rwlock_unlock(&vf->lock) == 0;
+#endif
+}
 
 #if SY_64
 static I fsize(F f){
  if(!f)R 0;
+
+ // Check if this is a virtual file
+ if(isvirtfile_fp(f)) {
+     VirtFile *vf = getvirtfile_fp(f);
+     if(!vf) R 0;
+     R vf->size;
+ }
+
+ // Regular file handling
 #if SY_WIN32
  R _filelengthi64(_fileno(f));
 #elif defined(__linux__)
@@ -79,6 +312,15 @@ static I fsize(F f){
 #else
 static I fsize(F f){
  RZ(f);
+
+ // Check if this is a virtual file
+ if(isvirtfile_fp(f)) {
+     VirtFile *vf = getvirtfile_fp(f);
+     if(!vf) R 0;
+     R vf->size;
+ }
+
+ // Regular file handling
 #if SY_WIN32
  R _filelength(_fileno(f));
 #else
@@ -89,6 +331,23 @@ static I fsize(F f){
 #endif
 
 static A jtrdns(J jt,F f){A za,z;I n=512;size_t r,tr=0;
+
+ // Check if this is a virtual file
+ if(isvirtfile_fp(f)) {
+     VirtFile *vf = getvirtfile_fp(f);
+     if(!vf) R 0;
+
+     // Read from current position to end of file (like real files)
+     I remaining = (vf->position >= vf->size) ? 0 : (vf->size - vf->position);
+     GATV0(z,LIT,remaining,1);
+     if(remaining > 0) {
+         MC(CAV1(z), vf->data + vf->position, remaining);
+         vf->position = vf->size; // Move position to end after reading
+     }
+     R z;
+ }
+
+ // Regular file handling
  GAT0(za,LIT,512,1); clearerr(f);
  NOUNROLL while(!feof(f) && (r=fread(CAV(za)+tr,sizeof(C),n-tr,f))){
   tr+=r; if(tr==(U)n){RZ(za=ext(0,za));n*=2;}
@@ -98,8 +357,56 @@ static A jtrdns(J jt,F f){A za,z;I n=512;size_t r,tr=0;
  R z;
 }    /* read entire file stream (non-seekable) */
 
+// Virtual file line reader - reads one line from current position
+static C* vf_fgets(C* buf, I size, VirtFile* vf) {
+    if(!vf || !buf || size <= 0 || vf->position >= vf->size) return NULL;
+
+    I remaining = vf->size - vf->position;
+    I read_max = (size - 1 < remaining) ? size - 1 : remaining; // Leave space for null terminator
+    I line_len = 0;
+
+    // Find end of line or end of data
+    C* start = vf->data + vf->position;
+    for(I i = 0; i < read_max; i++) {
+        buf[i] = start[i];
+        line_len++;
+        if(start[i] == '\n') {
+            break; // Include the newline in the result
+        }
+    }
+
+    buf[line_len] = '\0'; // Null terminate
+    vf->position += line_len; // Update position
+
+    return buf;
+}
+
 A jtrd(J jt,F f,I j,I n){A z;C*x;I p=0;size_t q=1;
  RZ(f);
+
+ // Check if this is a virtual file
+ if(isvirtfile_fp(f)) {
+     VirtFile *vf = getvirtfile_fp(f);
+     if(!vf) R 0;
+
+     if(0>n){if(j<0) n=-j; else n=vf->size-j;}
+
+     // Calculate read position
+     I pos = j + ((0>j) ? vf->size : 0);
+     if(pos < 0) pos = 0;
+     if(pos >= vf->size) n = 0;
+     else if(pos + n > vf->size) n = vf->size - pos;
+
+     // Read the data
+     GATV0(z,LIT,n,1);
+     if(n > 0) {
+         MC(CAV1(z), vf->data + pos, n);
+         vf->position = pos + n; // Update position after reading
+     }
+     R z;
+ }
+
+ // Regular file handling
  if(0>n){if(j<0) n=-j; else n=fsize(f)-j;}
 
 #if !SY_WINCE
@@ -119,6 +426,52 @@ A jtrd(J jt,F f,I j,I n){A z;C*x;I p=0;size_t q=1;
 
 static B jtwa(J jt,F f,I j,A w){C*x;I n,p=0;size_t q=1;
  RZ(f&&w);
+
+ // Check if this is a virtual file
+ if(isvirtfile_fp(f)) {
+     VirtFile *vf = getvirtfile_fp(f);
+     if(!vf) R 0;
+
+     n=AN(w)<<((AT(w)>>C2TX)&3); x=CAV(w);
+
+     // Calculate write position
+     // If j < 0, it's relative to end (like SEEK_END)
+     // If j >= file size, it's append/extend operation
+     I original_size = vf->size; // Remember original size before we modify it
+     I pos;
+     if(0>j) {
+         pos = vf->size + 1 + j; // -1 means append, -2 means one back from end, etc.
+     } else {
+         pos = j; // Absolute position
+     }
+     if(pos < 0) pos = 0;
+
+     // Ensure we have enough capacity
+     if(pos + n > vf->capacity) {
+         if(!expandvirtfile(vf, pos + n)) R 0;
+     }
+
+     // Update size if writing beyond current end
+     if(pos + n > vf->size) vf->size = pos + n;
+
+     // Write the data
+     memcpy(vf->data + pos, x, n);
+
+     // Update read position behavior:
+     // - For true positioned writes (j >= 0 and j != original_file_size), update position to after written data
+     // - For append operations (j == original_file_size or j < 0), preserve read position unchanged
+     if(j >= 0 && j != original_size) {
+         // True positioned write: set position to end of written data (like real files)
+         vf->position = pos + n;
+     }
+     // For appends (j == original file size or j < 0), deliberately do NOT update vf->position
+     // This preserves the read position across append operations (1!:3)
+     // which matches how real files behave - append doesn't affect read position
+
+     R 1;
+ }
+
+ // Regular file handling
 // obsolete  n=AN(w)*(C4T&AT(w)?4:C2T&AT(w)?2:1); x=CAV(w);
 n=AN(w)<<((AT(w)>>C2TX)&3); x=CAV(w);
 
@@ -127,10 +480,10 @@ n=AN(w)<<((AT(w)>>C2TX)&3); x=CAV(w);
 #else
  fseek(f,(long)(0>j?1+j:j),0>j?SEEK_END:SEEK_SET);
 #endif
- 
+
  clearerr(f);
  NOUNROLL while(q&&n>p){
-  p+=q=fwrite(p+x,sizeof(C),(size_t)(n-p),f); 
+  p+=q=fwrite(p+x,sizeof(C),(size_t)(n-p),f);
   if(ferror(f))R jerrno()?1:0;
  }
  R 1;
@@ -138,7 +491,7 @@ n=AN(w)<<((AT(w)>>C2TX)&3); x=CAV(w);
 
 // 1!:1 read entire file
 DF1(jtjfread){F12IP;A z;F f,fp;
- ASSERT(!JT(jt,seclev),EVSECURE) 
+ ASSERT(!JT(jt,seclev),EVSECURE)
  F1RANK(0,jtjfread,self);
  PROLOG(000);
  RE(f=stdf(w));  // f=file#, or 0 if w is a filename
@@ -148,7 +501,8 @@ DF1(jtjfread){F12IP;A z;F f,fp;
   RZ(fp=vfn(f));} //ensure actual file pointer
  else{RZ(fp=jope(w,FREAD_O));} //otherwise open named file
  I fl=fsize(fp);
- z=fl<0?rdns(fp):rd(fp,0,fl);
+ // For virtual files, always use rdns() to respect current position
+ z=(fl<0||isvirtfile_fp(fp))?rdns(fp):rd(fp,0,fl);
  if(!f){fclose(fp);} //if we opened the file, close it
  else{jtunvfn(jt,f,0);} //otherwise, release the lock on it
  EPILOG(z);}
@@ -190,16 +544,35 @@ DF1(jtjfreadline){F12IP;A z;F f,fp;C buf[1000];C *result;
   else RZ(fp=vfn(f));  // ensure actual file pointer
  }else{RZ(fp=jope(w,FREAD_O));}  // otherwise open named file
 
- // Use fgets to read one line
- result = fgets(buf, sizeof(buf), fp);
- if(result == NULL) {
-  // EOF or error - return empty string
-  GATV0(z,LIT,0,1);
+ // Check if this is a virtual file - use our custom line reader
+ if(isvirtfile_fp(fp)) {
+     VirtFile *vf = getvirtfile_fp(fp);
+     if(!vf) {
+         GATV0(z,LIT,0,1);
+     } else {
+         result = vf_fgets(buf, sizeof(buf), vf);
+         if(result == NULL) {
+             // EOF or error - return empty string
+             GATV0(z,LIT,0,1);
+         } else {
+             // Return the line (including newline if present)
+             I len = strlen(buf);
+             GATV0(z,LIT,len,1);
+             MC(CAV1(z),buf,len);
+         }
+     }
  } else {
-  // Return the line (including newline if present)
-  I len = strlen(buf);
-  GATV0(z,LIT,len,1);
-  MC(CAV1(z),buf,len);
+     // Use fgets to read one line from regular files
+     result = fgets(buf, sizeof(buf), fp);
+     if(result == NULL) {
+      // EOF or error - return empty string
+      GATV0(z,LIT,0,1);
+     } else {
+      // Return the line (including newline if present)
+      I len = strlen(buf);
+      GATV0(z,LIT,len,1);
+      MC(CAV1(z),buf,len);
+     }
  }
 
  if(!f){fclose(fp);} //if we opened the file, close it
@@ -219,20 +592,43 @@ DF1(jtjfreadlinetrim){F12IP;A z;F f,fp;C buf[1000];C *result;
   else RZ(fp=vfn(f));  // ensure actual file pointer
  }else{RZ(fp=jope(w,FREAD_O));}  // otherwise open named file
 
- // Use fgets to read one line from regular files
- result = fgets(buf, sizeof(buf), fp);
- if(result == NULL) {
-  // EOF or error - return empty string
-  GATV0(z,LIT,0,1);
+ // Check if this is a virtual file - use our custom line reader
+ if(isvirtfile_fp(fp)) {
+     VirtFile *vf = getvirtfile_fp(fp);
+     if(!vf) {
+         GATV0(z,LIT,0,1);
+     } else {
+         result = vf_fgets(buf, sizeof(buf), vf);
+         if(result == NULL) {
+             // EOF or error - return empty string
+             GATV0(z,LIT,0,1);
+         } else {
+             // Strip trailing CR/LF characters
+             I len = strlen(buf);
+             while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
+                len--;
+             }
+             // Return the line without newline characters
+             GATV0(z,LIT,len,1);
+             MC(CAV1(z),buf,len);
+         }
+     }
  } else {
-  // Strip trailing CR/LF characters
-  I len = strlen(buf);
-  while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
-   len--;
-  }
-  // Return the line without newline characters
-  GATV0(z,LIT,len,1);
-  MC(CAV1(z),buf,len);
+     // Use fgets to read one line from regular files
+     result = fgets(buf, sizeof(buf), fp);
+     if(result == NULL) {
+      // EOF or error - return empty string
+      GATV0(z,LIT,0,1);
+     } else {
+      // Strip trailing CR/LF characters
+      I len = strlen(buf);
+      while(len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) {
+       len--;
+      }
+      // Return the line without newline characters
+      GATV0(z,LIT,len,1);
+      MC(CAV1(z),buf,len);
+     }
  }
 
  if(!f){fclose(fp);} //if we opened the file, close it
@@ -292,6 +688,83 @@ DF1(jtjfreadlinepeek){F12IP;A z;F f,fp;C buf[1000];C *result;long pos;
  EPILOG(z);
 }
 
+// 1!:60 - create virtual RAM file
+DF1(jtjvramfile){F12IP;I virtid;A z,filename;C virtname[256];
+ ASSERT(!JT(jt,seclev),EVSECURE)
+ F1RANK(0,jtjvramfile,self);
+ PROLOG(000);
+
+ // Accept empty argument, plain string, or boxed string
+ if(!(ISSPARSE(AT(w)) ? AS(w)[0]==0&&AS(w)[1]==0 : !AN(w))) {
+     // Non-empty argument
+     if(LIT&AT(w)) {
+         // Plain string argument like 'testing'
+         I namelen = AN(w);
+         ASSERT(namelen > 0 && namelen < 200,EVLENGTH);
+         sprintf(virtname, "[VRAM:%.*s]", (int)namelen, CAV(w));
+     } else if(BOX&AT(w)) {
+         // Boxed string argument like <'testing'
+         ASSERT(1==AN(w),EVLENGTH);
+         A namestr = AAV(w)[0];
+         ASSERT(LIT&AT(namestr),EVDOMAIN);
+         I namelen = AN(namestr);
+         ASSERT(namelen > 0 && namelen < 200,EVLENGTH);
+         sprintf(virtname, "[VRAM:%.*s]", (int)namelen, CAV(namestr));
+     } else {
+         ASSERT(0,EVDOMAIN); // Invalid argument type
+     }
+ } else {
+     // Empty argument - use default name
+     sprintf(virtname, "[VRAM:auto]");
+ }
+
+ // Create virtual file
+ virtid = createvirtfile(jt);
+ if(!virtid) {
+     jsignal(EVWSFULL);  // Out of virtual file slots or memory
+     R 0;
+ }
+
+ // Add to default filename if we created successfully
+ if(strcmp(virtname, "[VRAM:auto]") == 0) {
+     sprintf(virtname, "[VRAM:auto-%ld]", (long)(virtid - nextvirtid));
+ }
+
+ // Create filename string for the file table - with error checking
+ filename = str(strlen(virtname), virtname);
+ if(!filename) {
+     closevirtfile(virtid);
+     jsignal(EVWSFULL);
+     R 0;
+ }
+
+ // Add to J's file table like a regular file - following jopen pattern
+ WRITELOCK(JT(jt,flock))
+
+ // Extend file table if needed (like jopen does)
+ while(AM(JT(jt,fopafl))==AN(JT(jt,fopafl)))RZ(jtextendunderlock(jt,&JT(jt,fopafl),&JT(jt,flock),0))
+
+ // Get virtual file for casting
+ VirtFile *vf = (VirtFile*)getvirtfile(virtid);
+ if(!vf) {
+     // This shouldn't happen since we just created it, but be safe
+     WRITEUNLOCK(JT(jt,flock))
+     closevirtfile(virtid);
+     jsignal(EVWSFULL);
+     R 0;
+ }
+
+ // Add filename to the file table - store virtual file ID in AM field
+ AAV0(JT(jt,fopafl))[AM(JT(jt,fopafl))] = filename; ACINITZAP(filename)
+ AM(filename) = virtid;  // Store virtual file ID in AM field (not the pointer!)
+ ++AM(JT(jt,fopafl));   // Increment file count
+
+ WRITEUNLOCK(JT(jt,flock))
+
+ // Return the file number as a scalar - use EPILOG for proper cleanup
+ EPILOG(sc(virtid));
+}
+
 // 1!:3
 DF2(jtjfappend){F12IP;B b;F f;
  ASSERT(!JT(jt,seclev),EVSECURE)
@@ -313,8 +786,8 @@ DF1(jtjfsize){F12IP;B b;F f;I m;
  F1RANK(0,jtjfsize,self);
  PROLOG(000);
  RE(f=stdf(w));
- if(b=!f)RZ(f=jope(w,FREAD_O)) else RE(vfn(f)); 
- m=fsize(f); 
+ if(b=!f)RZ(f=jope(w,FREAD_O)) else RE(vfn(f));
+ m=fsize(f);
  if(b)fclose(f);else{fflush(f); jtunvfn(jt,f,0);}  // if numbered file, remove the inuse mark
  RE(0); EPILOG(sc(m));   // check error, return mtm if none
 }
diff --git a/jsrc/xo.c b/jsrc/xo.c
index ab187461..1f510fe1 100644
--- a/jsrc/xo.c
+++ b/jsrc/xo.c
@@ -28,11 +28,34 @@ B jtxoinit(JS jjt){A x;JJ jt=MTHREAD(jjt);
 // check that x is in table of file#s.  Return x if so, otherwise 0 for error
 // if the file is found, mark it as busy in fopafl.  This establishes a lock on that file to prevent it from being deleted while I/O is in process
 // Every non-failing call to vfn must be matched by a call to unvfn to unlock the file
-F jtvfn(J jt,F x){F z=0; READLOCK(JT(jt,flock)) A*v=AAV0(JT(jt,fopafl)); DQ(AM(JT(jt,fopafl)),if((F)AM(v[i])==x){z=x; READLOCK(v[i]->lock) break;}) READUNLOCK(JT(jt,flock)) ASSERT(z!=0,EVFNUM); R z;}
+// Forward declarations for virtual file support
+extern B isvirtfile_fp(FILE* fp);
+
+// Forward declaration of VirtFile structure (full definition in xf.c)
+typedef struct VirtFile VirtFile;
+
+F jtvfn(J jt,F x){F z=0;
+ // Check if this is a virtual file first
+ if(isvirtfile_fp(x)) {
+     // For virtual files, the magic number check in isvirtfile_fp() is sufficient validation
+     // Virtual files manage their own locking internally, no need for J's file table locks
+     R x;  // Return the validated VirtFile pointer
+ }
+
+ // Regular file handling with original locking
+ READLOCK(JT(jt,flock)) A*v=AAV0(JT(jt,fopafl)); DQ(AM(JT(jt,fopafl)),if((F)AM(v[i])==x){z=x; READLOCK(v[i]->lock) break;}) READUNLOCK(JT(jt,flock)) ASSERT(z!=0,EVFNUM); R z;}
 
 // remove internal file lock set by vfn().  Required exactly when vfn has returned non0.  The file is known to be in the file table, but its index may have changed.
 // for syntactic ease we return the dummy argument
-A jtunvfn(J jt,F x,A dummy){READLOCK(JT(jt,flock)) A*v=AAV0(JT(jt,fopafl)); DQ(AM(JT(jt,fopafl)),if((F)AM(v[i])==x){READUNLOCK(v[i]->lock) break;}) READUNLOCK(JT(jt,flock)) R dummy;}
+A jtunvfn(J jt,F x,A dummy){
+ // Check if this is a virtual file first
+ if(isvirtfile_fp(x)) {
+     // Virtual files don't use J's string-based locking - they manage their own locks
+     R dummy;
+ }
+
+ // Regular file unlocking
+ READLOCK(JT(jt,flock)) A*v=AAV0(JT(jt,fopafl)); DQ(AM(JT(jt,fopafl)),if((F)AM(v[i])==x){READUNLOCK(v[i]->lock) break;}) READUNLOCK(JT(jt,flock)) R dummy;}
 
 // w is a user argument, either a number or a filename string.  If a number, return it (with error if it is 0); if a string return the file# if found, or 0 if not found
 // return of 0 is not ipso facto an error
@@ -171,6 +194,14 @@ F jtstdf(J jt,A w){A y;F f;I n,r,t;
   ASSERT(0,EVDOMAIN);
  }
  RE(f=(F)i0(w));   // extract integer
- ASSERT(f!=0,EVFNUM); 
+ ASSERT(f!=0,EVFNUM);
+
+ // Check if this is a virtual file ID
+ if((I)f >= 1000000) {  // Virtual file IDs start at 1000000
+     extern void* getvirtfile(I id);  // Forward declaration (returns VirtFile* but using void* to avoid typedef issues)
+     void* vf = getvirtfile((I)f);
+     if(vf) R (F)vf;  // Return VirtFile* cast as FILE*
+ }
+
  R f;
 }    /* 0 if w is a boxed file name; n if w is integer or boxed integer */

-- 
2.51.2

