SharedMemory.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 java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;

import org.newsclub.net.unix.AFSocket;
import org.newsclub.net.unix.FileChannelSupplier;
import org.newsclub.net.unix.FileDescriptorCast;
import org.newsclub.net.unix.MemoryImplUtilInternal;

import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * Some shared memory.
 *
 * @author Christian Kohlschütter
 */
@SuppressFBWarnings("OVERLY_PERMISSIVE_FILE_PERMISSION")
public final class SharedMemory implements Closeable {
  private static final Set<PosixFilePermission> DEFAULT_PERMISSIONS = PosixFilePermissions
      .fromString("rw-rw-rw-");

  /**
   * Keep track of known shared memory sizes, but only if useful (currently: Windows only).
   */
  static Map<FileDescriptor, Long> FD_MEMORY; // NOPMD

  /**
   * The exact size (in bytes) required for a {@link MemorySegment} used in
   * {@link #mutex(MemorySegment)}.
   */
  public static final int MUTEX_SEGMENT_SIZE = 8;

  /**
   * The exact size (in bytes) required for a {@link MemorySegment} used in
   * {@link #futex(MemorySegment)}.
   */
  static final int FUTEX32_SEGMENT_SIZE = 4;

  private static final Map<String, Integer> MAP_MODES = Map.of(//
      "READ_ONLY", MemoryImplUtilInternal.MMODE_READ, //
      "READ_WRITE", (MemoryImplUtilInternal.MMODE_READ | MemoryImplUtilInternal.MMODE_WRITE), //
      "PRIVATE", (MemoryImplUtilInternal.MMODE_READ | MemoryImplUtilInternal.MMODE_WRITE
          | MemoryImplUtilInternal.MMODE_COPY_ON_WRITE), //
      // from ExtendedMapMode:
      "READ_ONLY_SYNC", (MemoryImplUtilInternal.MMODE_READ | MemoryImplUtilInternal.MMODE_SYNC), //
      "READ_WRITE_SYNC", (MemoryImplUtilInternal.MMODE_READ | MemoryImplUtilInternal.MMODE_WRITE
          | MemoryImplUtilInternal.MMODE_SYNC) //
  );

  private final String name;

  private final boolean knownReadOnly;
  private final boolean unlinkUponClose;

  static MemoryImplUtilInternal UTIL = null; // NOPMD

  private final SharedMemoryCleaner cleaner;

  private final long size;

  private SharedMemory(FileDescriptor fd) throws IOException {
    this(fd, -1, null, 0);
  }

  private SharedMemory(FileDescriptor fd, long size, String name, int mopts) throws IOException {
    super();
    if (size == -1) {
      this.size = determineSize(fd);
    } else {
      this.size = size;
    }
    Objects.requireNonNull(fd);
    this.cleaner = new SharedMemoryCleaner(null, this, fd);
    this.name = name;
    this.knownReadOnly = isReadOnly(mopts);
    this.unlinkUponClose = (mopts & MemoryImplUtilInternal.MOPT_UNLINK_UPON_CLOSE) != 0;
  }

  private static long determineSize(FileDescriptor fd) throws IOException {
    Map<FileDescriptor, Long> map = FD_MEMORY;
    if (map != null) {
      synchronized (map) {
        Long knownSize = map.get(fd);
        if (knownSize != null) {
          return knownSize;
        }
      }
    }
    return getUtil().sizeOfSharedMemory(fd);
    // ; return FileDescriptorCast.using(fd).as(FileChannel.class).size();
  }

  static boolean isUtilLoaded() {
    return UTIL != null;
  }

  static MemoryImplUtilInternal getUtil() {
    AFSocket.isSupported(); // trigger init

    MemoryImplUtilInternal util = UTIL;
    if (util == null) {
      throw new IllegalStateException("MemoryImplUtilInternal not initialized");
    }
    return util;
  }

  /**
   * Internal initializer used by junixsocket-common; do not use.
   *
   * @param util The MemoryImplUtil instance.
   */
  public static synchronized void init(MemoryImplUtilInternal util) {
    if (util != null) {
      if (UTIL == null) {
        UTIL = util;
        if (util.needToTrackSharedMemory()) {
          FD_MEMORY = new WeakHashMap<>();
        } else {
          FD_MEMORY = null;
        }
      } else {
        throw new IllegalStateException();
      }
    }
  }

  /**
   * Creates a new {@link SharedMemory} instance using the given file descriptor, which can be
   * associated with a regular file that is to be memory-mapped, or a shared memory region.
   *
   * @param fd The file descriptor.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory using(FileDescriptor fd) throws IOException {
    return new SharedMemory(fd);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name, using default permissions
   * (read-write for all users, where applicable). If there already exists an object under that
   * name, this call fails with an error.
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createExclusively(String name, long minimumLength,
      SharedMemoryOption... options) throws IOException {
    return createExclusively(name, minimumLength, DEFAULT_PERMISSIONS, options);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name. If there already exists an
   * instance under that name, this call fails with an error.
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param perms The file system permissions, where applicable.
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createExclusively(String name, long minimumLength,
      Set<PosixFilePermission> perms, SharedMemoryOption... options) throws IOException {
    return shmOpen(name, perms, toOptions(options) | MemoryImplUtilInternal.MOPT_CREAT
        | MemoryImplUtilInternal.MOPT_EXCL, minimumLength);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name, using default permissions
   * (read-write for all users, where applicable). If there already exists an object under that
   * name, that object is opened instead.
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createOrOpenExisting(String name, long minimumLength,
      SharedMemoryOption... options) throws IOException {
    return createOrOpenExisting(name, minimumLength, DEFAULT_PERMISSIONS, options);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given namex. If there already exists an
   * object under that name, that object is opened instead.
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param perms The file system permissions, where applicable.
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createOrOpenExisting(String name, long minimumLength,
      Set<PosixFilePermission> perms, SharedMemoryOption... options) throws IOException {
    return shmOpen(name, perms, toOptions(options) | MemoryImplUtilInternal.MOPT_CREAT,
        minimumLength);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name, using default permissions
   * (read-write for all users, where applicable). If there already exists an object under that
   * name, that object is reused (truncated to zero or deleted prior to allocation).
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createOrReuseExisting(String name, long minimumLength,
      SharedMemoryOption... options) throws IOException {
    return createOrReuseExisting(name, minimumLength, DEFAULT_PERMISSIONS, options);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name. If there already exists an
   * object under that name, that object is reused (truncated to zero or deleted prior to
   * allocation).
   *
   * @param name The name.
   * @param minimumLength The requested length (the actual object can be larger).
   * @param perms The file system permissions, where applicable.
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createOrReuseExisting(String name, long minimumLength,
      Set<PosixFilePermission> perms, SharedMemoryOption... options) throws IOException {
    return shmOpen(name, perms, toOptions(options) | MemoryImplUtilInternal.MOPT_CREAT
        | MemoryImplUtilInternal.MOPT_TRUNC, minimumLength);
  }

  /**
   * Creates a new {@link SharedMemory} instance under the given name, using the object under the
   * given name. This call fails with an exception if no such object exists.
   *
   * @param name The name.
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory openExisting(String name, SharedMemoryOption... options)
      throws IOException {
    return shmOpen(name, DEFAULT_PERMISSIONS, toOptions(options), 0);
  }

  /**
   * Creates a new {@link SharedMemory} instance using an anonymous identifier.
   *
   * @param minimumLength The requested length (the actual object can be larger).
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createAnonymous(long minimumLength) throws IOException {
    return createAnonymous(minimumLength, (SharedMemoryOption[]) null);
  }

  /**
   * Creates a new {@link SharedMemory} instance using an anonymous identifier.
   *
   * @param minimumLength The requested length (the actual object can be larger).
   * @param options Instantiation options.
   * @return The new instance.
   * @throws IOException on error.
   */
  public static SharedMemory createAnonymous(long minimumLength, SharedMemoryOption... options)
      throws IOException {
    int mopts = toOptions(options) | MemoryImplUtilInternal.MOPT_CREAT
        | MemoryImplUtilInternal.MOPT_TRUNC;
    return shmOpen0(null, Collections.emptySet(), mopts, minimumLength);
  }

  private static SharedMemory shmOpen(String name, Set<PosixFilePermission> perms, int mopts,
      long minimumLength) throws IOException {
    name = checkShmName(name);
    return shmOpen0(name, perms, mopts, minimumLength);
  }

  @SuppressFBWarnings("USO_UNSAFE_ACCESSIBLE_OBJECT_SYNCHRONIZATION")
  private static SharedMemory shmOpen0(String name, Set<PosixFilePermission> perms, int mopts,
      long minimumLength) throws IOException {
    MemoryImplUtilInternal util = getUtil();

    FileDescriptor fd = new FileDescriptor();
    long size = util.shmOpen(fd, name, minimumLength, toMode(perms), mopts);
    SharedMemory sm = new SharedMemory(fd, size, name, mopts);

    Map<FileDescriptor, Long> map = FD_MEMORY;
    if (map != null) {
      synchronized (map) {
        map.put(fd, size);
      }
    }
    return sm;
  }

  private static boolean isReadOnly(int mopts) {
    return (mopts & MemoryImplUtilInternal.MOPT_RDONLY) != 0;
  }

  /**
   * Asks to explicitly unlink/remove a shared memory object identified by the given name.
   * <p>
   * This call may silently fail (some platforms do not support explicit unlinking -- they cleanup
   * the objects automatically).
   *
   * @param name The name of the object that should be unlinked.
   * @throws IOException on error.
   */
  public static void unlinkShared(String name) throws IOException {
    getUtil().shmUnlink(checkShmName(name));
  }

  /**
   * Returns the file descriptor associated with this instance.
   *
   * @return The file descriptor.
   */
  public FileDescriptor getFileDescriptor() {
    return cleaner.fd;
  }

  /**
   * Returns a {@link FileChannel} instance that can be used for memory-mapping via {code
   * FileChannel#map}. There are no guarantees that writing/truncating/mapping works, however
   * getting the current allocation size via {@link FileChannel#size()} should work.
   *
   * @return The {@link FileChannel}.
   * @throws IOException on error.
   * @throws ClosedChannelException if the file descriptor is closed.
   * @throws UnsupportedOperationException if this operation is not supported on this platform.
   */
  FileChannel asMappableFileChannel() throws IOException {
    if (!cleaner.fd.valid()) {
      throw new ClosedChannelException();
    }
    if (FD_MEMORY != null) {
      throw new UnsupportedOperationException();
    }
    if (knownReadOnly) {
      return FileDescriptorCast.using(cleaner.fd).as(FileChannelSupplier.ReadOnly.class).get();
    } else {
      return FileDescriptorCast.using(cleaner.fd).as(FileChannel.class);
    }
  }

  /**
   * Return a {@link MemorySegment} instance corresponding to this shared memory object, using the
   * given {@link MapMode}, and a custom shared {@link Arena} that will be closed upon
   * {@link SharedMemory#close()}.
   *
   * @param mapMode The map mode.
   * @return The memory segment.
   * @throws IOException on error.
   */
  public MemorySegment asMappedMemorySegment(MapMode mapMode) throws IOException {
    return asMappedMemorySegment(mapMode, null, 0);
  }

  /**
   * Return a {@link MemorySegment} instance corresponding to this shared memory object, using the
   * given {@link MapMode}, and the given arena.
   * <p>
   * If the given arena is {@code null}, a custom shared {@link Arena} is used that will be closed
   * upon {@link SharedMemory#close()}.
   *
   * @param mapMode The map mode.
   * @param arena The arena to use, or {@code null}.
   * @return The memory segment.
   * @throws IOException on error.
   */
  public MemorySegment asMappedMemorySegment(MapMode mapMode, Arena arena) throws IOException {
    return asMappedMemorySegment(mapMode, arena, 0);
  }

  /**
   * Return a {@link MemorySegment} instance corresponding to this shared memory object -- repeated
   * multiple times after each other (aligned with page size) -- using the given {@link MapMode},
   * and the given arena, as well as the duplication count.
   * <p>
   * This method is particularly useful to simplify building circular buffers ("magic RingBuffer").
   * <p>
   * If the given arena is {@code null}, a custom shared {@link Arena} is used that will be closed
   * upon {@link SharedMemory#close()}.
   *
   * @param mapMode The map mode.
   * @param arena The arena to use, or {@code null}.
   * @param duplicates The number of times the shared memory should be repeated (0 = no repetitions,
   *          just 1 copy).
   * @return The memory segment.
   * @throws IOException on error.
   */
  public MemorySegment asMappedMemorySegment(MapMode mapMode, Arena arena, int duplicates)
      throws IOException {
    return asMappedMemorySegment(mapMode, arena, 0, -1, duplicates);
  }

  /**
   * Return a {@link MemorySegment} instance corresponding to a range of this shared memory object
   * -- repeated multiple times after each other (aligned with page size) -- using the given
   * {@link MapMode}, and the given arena, as well as the duplication count.
   * <p>
   * This method is particularly useful to simplify building circular buffers ("magic RingBuffer").
   * <p>
   * If the given arena is {@code null}, a custom shared {@link Arena} is used that will be closed
   * upon {@link SharedMemory#close()}.
   *
   * @param mapMode The map mode.
   * @param arena The arena to use, or {@code null}.
   * @param offset The offset from the beginning of this segment, in bytes.
   * @param length The length of the mapped region, in bytes.
   * @param duplicates The number of times the shared memory should be repeated (0 = no repetitions,
   *          just 1 copy).
   * @return The memory segment.
   * @throws IOException on error.
   */
  public MemorySegment asMappedMemorySegment(MapMode mapMode, Arena arena, long offset, long length,
      int duplicates) throws IOException {
    if (offset < 0) {
      throw new IllegalArgumentException("startOffset");
    } else if (length < -1) {
      throw new IllegalArgumentException("length");
    }

    // use a zero-length, 0-address segment for lifecycle management, preventing chicken-egg problem
    MemorySegment arenaSegment;
    if (arena == null) {
      arenaSegment = cleaner.getArenaSegment();
    } else {
      arenaSegment = arena.allocate(0);
    }

    int mmode = resolveMmode(mapMode);
    if (length == -1) {
      // FileChannel fc = asMappableFileChannel();
      // long size = fc.size();

      length = size;
    }

    ByteBuffer buf = getUtil().mmapShm(arenaSegment, cleaner.fd, offset, length, mmode, duplicates);
    return asRegisteredMemorySegment(cleaner, buf, (mmode
        & MemoryImplUtilInternal.MMODE_WRITE) != 0, duplicates);
  }

  static MemorySegment asRegisteredMemorySegment(SharedMemoryCleaner cleaner, ByteBuffer buf,
      boolean rw) {
    return asRegisteredMemorySegment(cleaner, buf, rw, 0);
  }

  private static MemorySegment asRegisteredMemorySegment(SharedMemoryCleaner cleaner,
      ByteBuffer buf, boolean rw, int duplicates) {
    if (!rw) {
      // The MapMode is read-only.
      // If we don't ask for a read-only buffer here, write accesses will fail with a page fault
      // ("java.lang.InternalError: a fault occurred in an unsafe memory access operation")
      buf = buf.asReadOnlyBuffer();
    }
    MemorySegment ms = MemorySegment.ofBuffer(buf);
    cleaner.registerMemorySegment(ms, duplicates);
    return ms;
  }

  /**
   * Adds the given {@link MemorySeal}s, preventing certain operations on shared memory.
   *
   * @param seals The seals.
   * @throws IOException on error (e.g., if unsupported).
   */
  @SuppressWarnings("DoNotCallSuggester") // ErrorProne
  public void addSeals(Set<MemorySeal> seals) throws IOException {
    throw new IOException("Unsupported"); // FIXME
  }

  /**
   * Returns the current {@link MemorySeal}s for this shared memory instance.
   *
   * @return The seals, or empty if none or unsupported.
   * @throws IOException on error (e.g., if a system call fails unexpectedly).
   */
  public Set<MemorySeal> getSeals() throws IOException {
    return Collections.emptySet(); // FIXME
  }

  private static String checkShmName(String name) {
    Objects.requireNonNull(name);
    if (name.length() == 0) {
      throw new IllegalArgumentException("Name cannot be empty");
    }
    if (name.indexOf('/', 1) != -1) {
      throw new IllegalArgumentException("Name must not contain extra slashes");
    }
    if (name.charAt(0) == '/') {
      return name;
    } else {
      return "/" + name;
    }
  }

  private static int toMode(Set<PosixFilePermission> perms) {
    int mode = 0;
    if (perms == null) {
      perms = DEFAULT_PERMISSIONS;
    }
    for (PosixFilePermission perm : perms) {
      switch (perm) {
        case OWNER_READ:
          mode |= MemoryImplUtilInternal.S_IRUSR;
          break;
        case OWNER_WRITE:
          mode |= MemoryImplUtilInternal.S_IWUSR;
          break;
        case GROUP_READ:
          mode |= MemoryImplUtilInternal.S_IRGRP;
          break;
        case GROUP_WRITE:
          mode |= MemoryImplUtilInternal.S_IWGRP;
          break;
        case OTHERS_READ:
          mode |= MemoryImplUtilInternal.S_IROTH;
          break;
        case OTHERS_WRITE:
          mode |= MemoryImplUtilInternal.S_IWOTH;
          break;
        default:
          throw new IllegalArgumentException("Unsupported permission: " + perm);
      }
    }
    return mode;
  }

  private static int toOptions(SharedMemoryOption[] options) {
    int opt = 0;
    if (options != null) {
      for (SharedMemoryOption option : options) {
        opt |= option.getOpt();
      }
    }
    return opt;
  }

  /**
   * Closes this {@link SharedMemory} resource, potentially unlinking the corresponding underlying
   * resource from the kernel if the object has been instantiated with
   * {@link SharedMemoryOption#UNLINK_UPON_CLOSE}.
   */
  @Override
  public void close() throws IOException {
    boolean valid = cleaner.fd.valid();

    cleaner.close();
    if (unlinkUponClose && valid && name != null) {
      MemoryImplUtilInternal util = getUtil();
      util.shmUnlink(name);
    }
  }

  String getName() {
    return name;
  }

  private static int resolveMmode(MapMode mapMode) {
    String modeString = mapMode.toString();
    Integer mmode = MAP_MODES.get(modeString);
    if (mmode == null) {
      throw new UnsupportedOperationException("MapMode");
    }
    return mmode;
  }

  /**
   * 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 getUtil().getSharedMemoryAllocationSize();
  }

  Futex futex(MemorySegment addr) throws IOException {
    return futex(addr, false, false);
  }

  Futex futex(MemorySegment addr, boolean wakeUpOnClose) throws IOException {
    return futex(addr, wakeUpOnClose, false);
  }

  Futex futex(MemorySegment addr, boolean wakeUpOnClose, boolean zeroValueOnClose)
      throws IOException {
    if (addr.isReadOnly()) {
      throw new IOException("MemorySegment is read-only");
    }
    cleaner.checkCovered(addr);
    Futex futex = new Futex32(addr, zeroValueOnClose);
    if (wakeUpOnClose) {
      cleaner.registerFutex(futex);
    }
    return futex;
  }

  /**
   * Returns a {@link SharedMutex} instance working with the given {@link MemorySegment}, which has
   * to be exactly {@link #MUTEX_SEGMENT_SIZE} bytes long.
   *
   * @param addr The address.
   * @return The instance.
   * @throws IOException on error.
   */
  public SharedMutex mutex(MemorySegment addr) throws IOException {
    return mutex(addr, false);
  }

  private SharedMutex mutex(MemorySegment addr, boolean unlockOnClose) throws IOException {
    if (addr.isReadOnly()) {
      throw new IOException("MemorySegment is read-only");
    }
    if (addr.byteSize() != 8) {
      throw new IOException("MemorySegment must be exactly 8 bytes long");
    }

    cleaner.checkCovered(addr);

    Futex32 futex = new Futex32(addr.asSlice(0, 4), unlockOnClose);
    if (unlockOnClose) {
      cleaner.registerFutex(futex);
    }

    return futex.mutex();
  }

  /**
   * Returns the aligned size of this shared memory instance.
   *
   * @return The aligned size, in bytes.
   */
  public long byteSize() {
    return size;
  }
}