MappedMemory.java

/*
 * junixsocket
 *
 * Copyright 2009-2026 Christian Kohlschütter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.newsclub.net.unix.memory;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.ByteBuffer;

import org.newsclub.net.unix.MemoryImplUtilInternal;

import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * Some mapped memory.
 *
 * @author Christian Kohlschütter
 */
public final class MappedMemory implements Closeable {
  private final MemorySegment ms;
  private final SharedMemoryCleaner cleaner;
  private final boolean rw;
  private static final boolean IS_WINDOWS = ";".equals(System.getProperty("path.separator", ""));

  private MappedMemory(Arena arena, ByteBuffer mappedBuffer, boolean rw, FileDescriptor extraFd) {
    this.rw = rw;
    this.cleaner = new SharedMemoryCleaner(arena, this, extraFd);
    this.ms = SharedMemory.asRegisteredMemorySegment(cleaner, mappedBuffer, rw);
  }

  /**
   * Returns the {@link MemorySegment} backed by this instance.
   *
   * @return The {@link MemorySegment}.
   */
  @SuppressFBWarnings("EI_EXPOSE_REP")
  public MemorySegment getMemorySegment() {
    return ms;
  }

  /**
   * Returns the minimum sub-allocation size.
   *
   * @return The minimum sub-allocation size.
   */
  public long getMinimumSubAllocationSize() {
    return defaultAllocationSize();
  }

  /**
   * Returns the system's default memory page allocation size for shared memory.
   * <p>
   * This may be larger than the system's regular page size (e.g., on Windows it's 64k).
   *
   * @return The page size.
   */
  public static long defaultAllocationSize() {
    return SharedMemory.getUtil().getSharedMemoryAllocationSize();
  }

  /**
   * Returns an anonymous read-only memory segment of the given length.
   *
   * @param arena The arena to associate the {@link MappedMemory} instance with.
   * @param length The length of the segment.
   * @return The {@link MappedMemory} segment.
   * @throws IOException on error.
   */
  public static MappedMemory anonymousReadOnlySegment(Arena arena, long length) throws IOException {
    return anonymousSegment(arena, MemoryImplUtilInternal.MMODE_READ, length);
  }

  /**
   * Returns an anonymous read/write memory segment of the given length.
   *
   * @param arena The arena to associate the {@link MappedMemory} instance with.
   * @param length The length of the segment.
   * @return The {@link MappedMemory} segment.
   * @throws IOException on error.
   */
  public static MappedMemory anonymousSegment(Arena arena, long length) throws IOException {
    return anonymousSegment(arena, MemoryImplUtilInternal.MMODE_READ_WRITE, length);
  }

  /**
   * Returns an anonymous memory segment of the given length that acts as a placeholder for later,
   * memory-mapped segments in this area.
   *
   * @param arena The arena to associate the {@link MappedMemory} instance with.
   * @param length The length of the segment.
   * @return The {@link MappedMemory} segment.
   * @throws IOException on error.
   */
  public static MappedMemory placeholderSegment(Arena arena, long length) throws IOException {
    return anonymousSegment(arena, MemoryImplUtilInternal.MMODE_PLACEHOLDER
        | MemoryImplUtilInternal.MMODE_READ_WRITE, length);
  }

  /**
   * Returns an anonymous copy-on-write (changes are local to the process running this JVM) memory
   * segment of the given length.
   *
   * @param arena The arena to associate the {@link MappedMemory} instance with.
   * @param length The length of the segment.
   * @return The {@link MappedMemory} segment.
   * @throws IOException on error.
   */
  public static MappedMemory anonymousCopyOnWriteSegment(Arena arena, long length)
      throws IOException {
    return anonymousSegment(arena, MemoryImplUtilInternal.MMODE_READ_WRITE
        | MemoryImplUtilInternal.MMODE_COPY_ON_WRITE, length);
  }

  private static MappedMemory anonymousSegment(Arena arena, int mode, long length)
      throws IOException {
    MemoryImplUtilInternal util = SharedMemory.getUtil();

    mode |= MemoryImplUtilInternal.MMODE_ANONYMOUS;

    long addr = util.mmap(0, null, 0, length, mode, null);
    ByteBuffer mappedBuffer = util.mappedBuffer(addr, length, null, 0, arena.allocate(0));

    return new MappedMemory(arena, mappedBuffer, (mode & MemoryImplUtilInternal.MMODE_WRITE) != 0,
        null);
  }

  /**
   * Maps some memory to a region of this {@link MappedMemory} object. This may fail to work on
   * Windows, unless {@link #placeholderSegment(Arena, long)} was used to create this
   * {@link MappedMemory} instance.
   *
   * @param msOffset The target offset in this object.
   * @param length The number of bytes to map.
   * @param fd The file descriptor to map from.
   * @param fdOffset The file descriptor-specific content offset
   * @return A new {@link MappedMemory} object, whose {@link MemorySegment} is a subrange of this
   *         object's {@link MemorySegment}.
   * @throws IOException on error.
   * @see #placeholderSegment(Arena, long)
   */
  public MappedMemory mapRegion(long msOffset, long length, FileDescriptor fd, long fdOffset)
      throws IOException {
    if (msOffset < 0) {
      throw new IllegalArgumentException("msOffset");
    }
    if (length <= 0) {
      throw new IllegalArgumentException("length");
    }
    if ((msOffset + length) > ms.byteSize()) {
      throw new IllegalArgumentException("capacity");
    }
    if (fdOffset < 0) {
      throw new IllegalArgumentException("fileOffset");
    }

    MemoryImplUtilInternal util = SharedMemory.getUtil();

    int mmode = ms.isReadOnly() ? MemoryImplUtilInternal.MMODE_READ
        : MemoryImplUtilInternal.MMODE_READ_WRITE;
    mmode |= MemoryImplUtilInternal.MMODE_FIXED;
    // FIXME cow

    long targetAddr = ms.address() + msOffset;

    FileDescriptor extraFd = IS_WINDOWS ? new FileDescriptor() : null;
    long actualAddr = util.mmap(targetAddr, fd, fdOffset, length, mmode, extraFd);

    if (actualAddr != targetAddr) {
      // unexpected
      util.unmap(targetAddr, length, 0, true);
      throw new IllegalStateException("targetAddr " + actualAddr + " vs expected " + targetAddr);
    }

    ByteBuffer mappedBuffer = util.mappedBuffer(actualAddr, length, null, 0, cleaner
        .getArenaSegment());

    return new MappedMemory(cleaner.getArena(), mappedBuffer, rw, extraFd);
  }

  @Override
  public void close() throws IOException {
    cleaner.close();
  }
}