/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2023 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/assert.h"
#include "libc/atomic.h"
#include "libc/calls/blockcancel.internal.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/struct/timespec.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/cosmo.h"
#include "libc/dce.h"
#include "libc/dlopen/dlfcn.h"
#include "libc/elf/def.h"
#include "libc/elf/elf.h"
#include "libc/elf/scalar.h"
#include "libc/elf/struct/auxv.h"
#include "libc/elf/struct/ehdr.h"
#include "libc/elf/struct/phdr.h"
#include "libc/errno.h"
#include "libc/fmt/itoa.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/kprintf.h"
#include "libc/intrin/strace.h"
#include "libc/limits.h"
#include "libc/nt/dll.h"
#include "libc/nt/enum/filemapflags.h"
#include "libc/nt/enum/pageflags.h"
#include "libc/nt/memory.h"
#include "libc/nt/runtime.h"
#include "libc/proc/posix_spawn.h"
#include "libc/runtime/internal.h"
#include "libc/runtime/runtime.h"
#include "libc/runtime/syslib.internal.h"
#include "libc/serialize.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/auxv.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/errfuns.h"
#include "libc/temp.h"
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"

/**
 * @fileoverview Cosmopolitan Dynamic Linker.
 *
 * Every program built using Cosmopolitan is statically-linked. However
 * there are some cases, e.g. GUIs and video drivers, where linking the
 * host platform libraries is desirable. So what we do in such cases is
 * launch a stub executable using the host platform's libc, and longjmp
 * back into this executable. The stub executable passes back to us the
 * platform-specific dlopen() implementation, which we shall then wrap.
 *
 * @kudos jacereda for figuring out how to do this
 */

#define AMD_REXB    0x41
#define AMD_REXW    0x48
#define AMD_MOV_IMM 0xb8

#define ARM_REG_OFF 0
#define ARM_IMM_OFF 5
#define ARM_IDX_OFF 21
#define ARM_MOV_NEX 0xf2800000u

#define XNU_RTLD_LAZY   1
#define XNU_RTLD_NOW    2
#define XNU_RTLD_LOCAL  4
#define XNU_RTLD_GLOBAL 8

#define HELPER \
  "#include <dlfcn.h>\n\
#include <stdio.h>\n\
#include <stdlib.h>\n\
int main(int argc, char **argv, char **envp) {\n\
  char *ep;\n\
  long addr;\n\
  if (argc != 2) {\n\
    fprintf(stderr, \"%s: not intended to be run directly\\n\", argv[0]);\n\
    return 1;\n\
  }\n\
  addr = strtol(argv[1], &ep, 10);\n\
  if (*ep) {\n\
    fprintf(stderr, \"%s: invalid function address\\n\", argv[0]);\n\
    return 2;\n\
  }\n\
  return ((int (*)(void *))addr)((void *[]){\n\
      dlopen,\n\
      dlsym,\n\
      dlclose,\n\
      dlerror,\n\
  });\n\
}\n"

struct Loaded {
  char *base;
  char *entry;
  Elf64_Ehdr eh;
  Elf64_Phdr ph[25];
};

struct {
  atomic_uint once;
  bool is_supported;
  struct CosmoTib *tib;
  void *(*dlopen)(const char *, int);
  void *(*dlsym)(void *, const char *);
  int (*dlclose)(void *);
  char *(*dlerror)(void);
  jmp_buf jb;
} __foreign;

long __sysv2nt14();
long foreign_tramp();
void __dlopen_lock(void);
void __dlopen_unlock(void);

static _Thread_local char dlerror_buf[128];

static const char *get_tmp_dir(void) {
  const char *tmpdir;
  if (!(tmpdir = getenv("TMPDIR")) || !*tmpdir)
    if (!(tmpdir = getenv("HOME")) || !*tmpdir)
      tmpdir = ".";
  return tmpdir;
}

static int is_file_newer_than(const char *path, const char *other) {
  struct stat st1, st2;
  if (stat(path, &st1))
    return -1;
  if (stat(other, &st2)) {
    if (errno == ENOENT) {
      return 2;
    } else {
      return -1;
    }
  }
  return timespec_cmp(st1.st_mtim, st2.st_mtim) > 0;
}

static unsigned elf2prot(unsigned x) {
  unsigned r = 0;
  if (x & PF_R)
    r += PROT_READ;
  if (x & PF_W)
    r += PROT_WRITE;
  if (x & PF_X)
    r += PROT_EXEC;
  return r;
}

static int get_host_elf_machine(void) {
#ifdef __x86_64__
  return EM_NEXGEN32E;
#elif defined(__aarch64__)
  return EM_AARCH64;
#elif defined(__powerpc64__)
  return EM_PPC64;
#elif defined(__riscv)
  return EM_RISCV;
#elif defined(__s390x__)
  return EM_S390;
#else
#error "unsupported architecture"
#endif
}

static char *elf_map(int fd, Elf64_Ehdr *ehdr, Elf64_Phdr *phdr, long pagesz,
                     char *interp_path, size_t interp_size) {
  Elf64_Addr maxva = 0;
  Elf64_Addr minva = -1;
  for (Elf64_Phdr *p = phdr; p < phdr + ehdr->e_phnum; p++) {
    if (p->p_type != PT_LOAD)
      continue;
    if (p->p_vaddr < minva)
      minva = p->p_vaddr & -pagesz;
    if (p->p_vaddr + p->p_memsz > maxva)
      maxva = p->p_vaddr + p->p_memsz;
  }
  uint8_t *base =
      __sys_mmap(0, maxva - minva, PROT_NONE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0, 0);
  if (base == MAP_FAILED)
    return MAP_FAILED;
  for (Elf64_Phdr *p = phdr; p < phdr + ehdr->e_phnum; p++) {
    if (p->p_type != PT_LOAD) {
      if (p->p_type == PT_INTERP && interp_size &&
          (p->p_filesz >= interp_size - 1 ||
           pread(fd, interp_path, p->p_filesz, p->p_offset) != p->p_filesz))
        return MAP_FAILED;
      continue;
    }
    Elf64_Addr skew = p->p_vaddr & (pagesz - 1);
    Elf64_Off off = p->p_offset - skew;
    Elf64_Addr a = p->p_vaddr + p->p_filesz;
    Elf64_Addr b = (a + (pagesz - 1)) & -pagesz;
    Elf64_Addr c = p->p_vaddr + p->p_memsz;
    int prot2 = elf2prot(p->p_flags);
    int prot1 = prot2;
    if (b > a) {
      prot1 |= PROT_WRITE;
      prot1 &= ~PROT_EXEC;
    }
    if (__sys_mmap(base + p->p_vaddr - skew, skew + p->p_filesz, prot1,
                   MAP_FIXED | MAP_PRIVATE, fd, off, off) == MAP_FAILED)
      return MAP_FAILED;
    if (b > a)
      bzero(base + a, b - a);
    if (c > b && __sys_mmap(base + b, c - b, prot2,
                            MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0,
                            0) == MAP_FAILED)
      return MAP_FAILED;
    if (prot1 != prot2 &&
        sys_mprotect(base + p->p_vaddr - skew, skew + p->p_filesz, prot2))
      return MAP_FAILED;
  }
  return (void *)base;
}

static bool elf_slurp(struct Loaded *l, int fd, const char *file) {
  if (pread(fd, &l->eh, 64, 0) != 64)
    return false;
  if (!IsElf64Binary(&l->eh, 64) ||                      //
      l->eh.e_phnum > sizeof(l->ph) / sizeof(*l->ph) ||  //
      l->eh.e_machine != get_host_elf_machine()) {
    enoexec();
    return false;
  }
  int bytes = l->eh.e_phnum * sizeof(l->ph[0]);
  if (pread(fd, l->ph, bytes, l->eh.e_phoff) != bytes)
    return false;
  l->entry = (char *)l->eh.e_entry;
  return true;
}

dontinline static bool elf_load(struct Loaded *l, const char *file, long pagesz,
                                char *interp_path, size_t interp_size) {
  int fd;
  if ((fd = open(file, O_RDONLY | O_CLOEXEC)) == -1)
    return false;
  if (!elf_slurp(l, fd, file)) {
    close(fd);
    return false;
  }
  if ((l->base = elf_map(fd, &l->eh, l->ph, pagesz, interp_path,
                         interp_size)) == MAP_FAILED) {
    close(fd);
    return false;
  }
  l->entry += (uintptr_t)l->base;
  close(fd);
  return true;
}

static long *push_strs(long *sp, char **list, int count) {
  *--sp = 0;
  while (count)
    *--sp = (long)list[--count];
  return sp;
}

wontreturn dontinstrument static void foreign_helper(void **p) {
  __foreign.dlopen = p[0];
  __foreign.dlsym = p[1];
  __foreign.dlclose = p[2];
  __foreign.dlerror = p[3];
  _longjmp(__foreign.jb, 1);
}

dontinline static void elf_exec(const char *file, char **envp) {

  // get microprocessor page size
  long pagesz = __pagesize;

  // load helper executable into address space
  struct Loaded prog;
  char interp_path[256] = {0};
  if (!elf_load(&prog, file, pagesz, interp_path, sizeof(interp_path)))
    return;

  // load platform c library into address space
  struct Loaded interp;
  if (!elf_load(&interp, interp_path, pagesz, 0, 0))
    return;

  // count environment variables
  int envc = 0;
  while (envp[envc])
    envc++;

  // count auxiliary values
  int auxc = 0;
  Elf64_auxv_t *av;
  for (av = (Elf64_auxv_t *)__auxv; av->a_type; ++av)
    auxc++;

  // create environment block for embedded process
  // the platform libc will save its location for getenv(), etc.
  // we need just enough stack memory beneath it for initialization
  char *map;
  size_t stksize = 65536;
  size_t stkalign = 8 * 2;
  size_t argsize = (1 + 2 + 1 + envc + 1 + auxc * 2 + 1 + 3) * 8;
  size_t mapsize = (stksize + argsize + (pagesz - 1)) & -pagesz;
  size_t skew = (mapsize - argsize) & (stkalign - 1);
  if (IsFreebsd())
    skew += 8;  // FreeBSD calls _start() like a C function
  map = __sys_mmap(0, mapsize, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0, 0);
  if (map == MAP_FAILED)
    return;
  long *sp = (long *)(map + mapsize - skew);

  // push argument string
  char *address_argument = (char *)(sp -= 3);
  FormatInt64(address_argument, (uintptr_t)foreign_helper);

  // push auxiliary values
  // these tell the platform libc how to load the executable
  *--sp = 0;
  unsigned long key, val;
  for (av = (Elf64_auxv_t *)__auxv; (key = av->a_type); ++av) {
    val = av->a_un.a_val;
    if (key == AT_PHDR)
      val = (long)(prog.base + prog.eh.e_phoff);
    if (key == AT_PHENT)
      val = prog.eh.e_phentsize;
    if (key == AT_PHNUM)
      val = prog.eh.e_phnum;
    if (key == AT_PAGESZ)
      val = pagesz;
    if (key == AT_BASE)
      val = (long)interp.base;
    if (key == AT_FLAGS)
      val = 0;
    if (key == AT_ENTRY)
      val = (long)prog.entry;
    if (key == AT_EXECFN)
      val = (long)program_invocation_name;
    *--sp = val;
    *--sp = key;
  }

  // push environment variable pointers
  sp = push_strs(sp, envp, envc);
  envp = (char **)sp;

  // push argument pointers
  *--sp = 0;
  *--sp = (long)address_argument;
  *--sp = (long)program_invocation_name;
  char **argv = (char **)sp;
  (void)argv;
  int argc = 2;
  *--sp = argc;

  STRACE("running dlopen importer %p...", interp.entry);

  // XXX: ideally we should set most registers to zero
#ifdef __x86_64__
  struct ps_strings {
    char **argv;
    int argc;
    char **envp;
    int envc;
  } pss = {argv, argc, envp, envc};
  asm volatile("mov\t%2,%%rsp\n\t"
               "jmpq\t*%1"
               : /* no outputs */
               : "D"(IsFreebsd() ? sp : 0), "S"(interp.entry), "d"(sp),
                 "b"(IsNetbsd() ? &pss : 0)
               : "memory");
  __builtin_unreachable();
#elif defined(__aarch64__)
  register long x0 asm("x0") = IsFreebsd() ? (long)sp : 0;
  register long x9 asm("x9") = (long)sp;
  register long x16 asm("x16") = (long)interp.entry;
  asm volatile("mov\tsp,x9\n\t"
               "br\tx16"
               : /* no outputs */
               : "r"(x0), "r"(x9), "r"(x16)
               : "memory");
  __builtin_unreachable();
#else
#error "unsupported architecture"
#endif
}

static char *dlerror_set(const char *str) {
  strlcpy(dlerror_buf, str, sizeof(dlerror_buf));
  return dlerror_buf;
}

dontinline static char *foreign_alloc_block(void) {
  char *p = 0;
  size_t sz = 65536;
  if (!IsWindows()) {
    p = __sys_mmap(0, sz, PROT_READ | PROT_WRITE | PROT_EXEC,
                   MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT, -1, 0, 0);
    if (p == MAP_FAILED)
      p = 0;
  } else {
    uintptr_t h;
    if ((h = CreateFileMapping(-1, 0, kNtPageExecuteReadwrite, 0, sz, 0))) {
      p = MapViewOfFileEx(h, kNtFileMapWrite | kNtFileMapExecute, 0, 0, sz, 0);
      CloseHandle(h);
    }
  }
  if (p) {
    WRITE32LE(p, 4);  // store used index
  } else {
    dlerror_set("out of memory");
  }
  return p;
}

dontinline static void *foreign_alloc(size_t n) {
  void *res;
  static char *block;
  __dlopen_lock();
  if (!block || READ32LE(block) + n > 65536)
    if (!(block = foreign_alloc_block()))
      return 0;
  res = block + READ32LE(block);
  WRITE32LE(block, READ32LE(block) + n);
  __dlopen_unlock();
  return res;
}

static uint8_t *movimm(uint8_t p[static 16], int reg, uint64_t val) {
#ifdef __x86_64__
  int rex;
  rex = AMD_REXW;
  if (reg & 8)
    rex |= AMD_REXB;
  *p++ = rex;
  *p++ = AMD_MOV_IMM | (reg & 7);
  p = WRITE64LE(p, val);
#elif defined(__aarch64__)
  // ARM immediate moves are encoded as:
  //
  //     ┌64-bit
  //     │
  //     │┌{sign,???,zero,non}-extending
  //     ││
  //     ││       ┌short[4] index
  //     ││       │
  //     ││  MOV  │    immediate   register
  //     │├┐┌─┴──┐├┐┌──────┴───────┐┌─┴─┐
  //   0bmxx100101iivvvvvvvvvvvvvvvvrrrrr
  //
  // Which allows 16 bits to be loaded into a register at a time, with
  // tricks for clearing other parts of the register. For example, the
  // sign-extending mode will set the higher order shorts to all ones,
  // and it expects the immediate to be encoded using ones' complement
  uint32_t op;
  for (unsigned i = 0; i < 4; ++i) {
    op = ARM_MOV_NEX;
    op |= i << ARM_IDX_OFF;
    op |= reg << ARM_REG_OFF;
    op |= (val & 0xffff) << ARM_IMM_OFF;
    val >>= 16;
    *(uint32_t *)p = op;
    p += sizeof(uint32_t);
  }
#else
#error "unsupported architecture"
#endif
  return p;
}

static void *foreign_thunk_sysv(void *func) {
  uint8_t *code, *p;
#ifdef __x86_64__
  // it is no longer needed
  if (1)
    return func;
  // movabs $func,%rax
  // movabs $foreign_tramp,%r10
  // jmp *%r10
  if (!(p = code = foreign_alloc(23)))
    return 0;  // 10 + 10 + 3 = 23
  p = movimm(p, 0, (uintptr_t)func);
  p = movimm(p, 10, (uintptr_t)foreign_tramp);
  *p++ = 0x41;
  *p++ = 0xff;
  *p++ = 0xe2;
#elif defined(__aarch64__)
  __jit_begin();
  if ((p = code = foreign_alloc(36))) {
    p = movimm(p, 8, (uintptr_t)func);
    p = movimm(p, 10, (uintptr_t)foreign_tramp);
    *(uint32_t *)p = 0xd61f0140;  // br x10
    __clear_cache(code, p + 4);
  }
  __jit_end();
#else
#error "unsupported architecture"
#endif
  return code;
}

static void *foreign_thunk_nt(void *func) {
  uint8_t *code;
  if (!(code = foreign_alloc(27)))
    return 0;
  // push %rbp
  code[0] = 0x55;
  // mov %rsp,%rbp
  code[1] = 0x48;
  code[2] = 0x89;
  code[3] = 0xe5;
  // movabs $func,%rax
  code[4] = 0x48;
  code[5] = 0xb8;
  WRITE64LE(code + 6, (uintptr_t)func);
  // movabs $tramp,%r10
  code[14] = 0x49;
  code[15] = 0xba;
#ifdef __x86_64__
  WRITE64LE(code + 16, (uintptr_t)__sysv2nt14);
#endif
  // jmp *%r10
  code[24] = 0x41;
  code[25] = 0xff;
  code[26] = 0xe2;
  return code;
}

dontinline static bool foreign_compile(char exe[hasatleast PATH_MAX]) {

  // construct path
  strlcpy(exe, get_tmp_dir(), PATH_MAX);
  strlcat(exe, "/.cosmo/", PATH_MAX);
  if (mkdir(exe, 0755) && errno != EEXIST)
    return false;
  strlcat(exe, "dlopen-helper", PATH_MAX);

  // skip build if helper exists and this program is older
  bool helper_exe_exists;
  switch (is_file_newer_than(GetProgramExecutableName(), exe)) {
    case -1:
      return false;
    case 0:
      return true;
    case 1:
      helper_exe_exists = true;
      break;
    case 2:
      helper_exe_exists = false;
      break;
    default:
      __builtin_unreachable();
  }

  // skip build if helper has same source code
  int fd;
  char src[PATH_MAX];
  char sauce[sizeof(HELPER)];
  strlcpy(src, exe, PATH_MAX);
  strlcat(src, ".c", PATH_MAX);
  if (helper_exe_exists) {
    if ((fd = open(src, O_RDONLY | O_CLOEXEC)) != -1) {
      ssize_t got = pread(fd, sauce, sizeof(HELPER), 0);
      close(fd);
      if (got == sizeof(HELPER) - 1 &&
          !memcmp(sauce, HELPER, sizeof(HELPER) - 1))
        return true;
    }
  }

  // create source file
  char tmp[PATH_MAX];
  strlcpy(tmp, src, PATH_MAX);
  strlcat(tmp, ".XXXXXX", PATH_MAX);
  if ((fd = mkostemp(tmp, O_CLOEXEC)) == -1)
    return false;
  if (write(fd, HELPER, sizeof(HELPER) - 1) != sizeof(HELPER) - 1) {
    close(fd);
    unlink(tmp);
    return false;
  }
  if (close(fd)) {
    unlink(tmp);
    return false;
  }
  if (rename(tmp, src)) {
    unlink(tmp);
    return false;
  }

  // create executable
  strlcpy(tmp, exe, PATH_MAX);
  strlcat(tmp, ".XXXXXX", PATH_MAX);
  if ((fd = mkostemp(tmp, O_CLOEXEC)) == -1)
    return false;
  int pid, ws;
  char *args[] = {
      "cc",
      "-pie",
      "-fPIC",
      src,
      "-o",
      tmp,
      IsLinux() ? "-Wl,-z,execstack" : "-DIGNORE",
      IsNetbsd() ? 0 : "-ldl",
      0,
  };
  errno_t err = posix_spawnp(&pid, args[0], NULL, NULL, args, environ);
  if (err) {
    unlink(tmp);
    errno = err;
    return false;
  }
  if (waitpid(pid, &ws, 0) == -1) {
    // signals and cancelation are blocked
    // therefore this must be a real error
    unlink(tmp);
    return false;
  }
  if (ws) {
    unlink(tmp);
    return false;
  }
  if (rename(tmp, exe)) {
    unlink(tmp);
    return false;
  }
  return true;
}

static bool foreign_setup(void) {
  // geth path of helper executable
  char exe[PATH_MAX];
  if (!foreign_compile(exe)) {
    return false;
  }
  // load helper executable into address space
#ifdef __x86_64__
  struct CosmoTib *cosmo_tib = __get_tls();
#endif
  if (!setjmp(__foreign.jb)) {
    elf_exec(exe, environ);
    return false;  // if elf_exec() returns, it failed
  }
#ifdef __x86_64__
  __foreign.tib = __get_tls();
  __set_tls(cosmo_tib);
#endif
  __foreign.dlopen = foreign_thunk_sysv(__foreign.dlopen);
  __foreign.dlsym = foreign_thunk_sysv(__foreign.dlsym);
  __foreign.dlclose = foreign_thunk_sysv(__foreign.dlclose);
  __foreign.dlerror = foreign_thunk_sysv(__foreign.dlerror);
  __foreign.is_supported = true;
  return true;
}

static void foreign_once(void) {
  foreign_setup();
}

static bool foreign_init(void) {
  bool res;
  cosmo_once(&__foreign.once, foreign_once);
  if (!(res = __foreign.is_supported))
    dlerror_set("dlopen() isn't supported on this platform");
  return res;
}

static int dlclose_nt(void *handle) {
  int res;
  if (FreeLibrary((uintptr_t)handle)) {
    res = 0;
  } else {
    dlerror_set("FreeLibrary() failed");
    res = -1;
  }
  return res;
}

static void *dlopen_nt(const char *path, int mode) {
  int n;
  uintptr_t handle;
  char16_t path16[PATH_MAX + 2];
  if (mode & ~(RTLD_LOCAL | RTLD_LAZY | RTLD_NOW)) {
    dlerror_set("invalid mode");
    return 0;
  }
  if ((n = __mkntpath(path, path16)) == -1) {
    dlerror_set("path invalid");
    return 0;
  }
  if (n > 3 &&                 //
      path16[n - 3] == '.' &&  //
      path16[n - 2] == 's' &&  //
      path16[n - 1] == 'o') {
    path16[n - 2] = 'd';
    path16[n - 1] = 'l';
    path16[n + 0] = 'l';
    path16[n + 1] = 0;
  }
  if (!(handle = LoadLibrary(path16)))
    dlerror_set("library not found");
  return (void *)handle;
}

static void *dlsym_nt(void *handle, const char *name) {
  void *x64_abi_func;
  if ((x64_abi_func = GetProcAddress((uintptr_t)handle, name))) {
    return x64_abi_func;
  } else {
    dlerror_set("symbol not found: ");
    strlcat(dlerror_buf, name, sizeof(dlerror_buf));
    return 0;
  }
}

static void *dlopen_silicon(const char *path, int mode) {
  int n;
  int xnu_mode = 0;
  char path2[PATH_MAX + 5];
  if (mode & ~(RTLD_LOCAL | RTLD_LAZY | RTLD_NOW | RTLD_GLOBAL))
    xnu_mode = -1;  // punt error to system dlerror() impl
  if (!(mode & RTLD_GLOBAL))
    xnu_mode |= XNU_RTLD_LOCAL;  // unlike Linux, XNU defaults to RTLD_GLOBAL
  if (mode & RTLD_NOW)
    xnu_mode |= XNU_RTLD_NOW;
  if (mode & RTLD_LAZY)
    xnu_mode |= XNU_RTLD_LAZY;
  if ((n = strlen(path)) < PATH_MAX && n > 3 &&  //
      path[n - 3] == '.' &&                      //
      path[n - 2] == 's' &&                      //
      path[n - 1] == 'o') {
    memcpy(path2, path, n);
    path2[n - 2] = 'd';
    path2[n - 1] = 'y';
    path2[n + 0] = 'l';
    path2[n + 1] = 'i';
    path2[n + 2] = 'b';
    path2[n + 3] = 0;
    path = path2;
  }
  return __syslib->__dlopen(path, xnu_mode);
}

/**
 * Opens dynamic shared object using host platform libc.
 *
 * If a `path` ending with `.so` is passed on Windows or MacOS, then
 * this wrapper will automatically change it to `.dll` or `.dylib` to
 * increase its chance of successfully loading.
 *
 * WARNING: Our API uses a different naming because cosmo_dlopen() lacks
 * many of the features one would reasonably expect from a UNIX dlopen()
 * implementation; and we don't want to lead ./configure scripts astray.
 * Foreign libraries also can't link symbols defined by your executable,
 * which means using this for high-level language plugins is completely
 * out of the question. What cosmo_dlopen() can do is help you talk to
 * GPU and GUI libraries like CUDA and SDL.
 *
 * @param mode is a bitmask that can contain:
 *     - `RTLD_LOCAL` (default)
 *     - `RTLD_GLOBAL` (not supported on Windows)
 *     - `RTLD_LAZY`
 *     - `RTLD_NOW`
 * @return dso handle, or NULL w/ dlerror()
 * @note this non-standard API is feature gated; you need to pass the
 *     `-mcosmo` flag to `cosmocc` so that `<dlfcn.h>` will define it
 */
void *cosmo_dlopen(const char *path, int mode) {
  void *res;
  BLOCK_SIGNALS;
  BLOCK_CANCELATION;
  if (IsWindows()) {
    res = dlopen_nt(path, mode);
  } else if (IsXnuSilicon()) {
    res = dlopen_silicon(path, mode);
  } else if (IsXnu()) {
    dlerror_set("dlopen() isn't supported on x86-64 MacOS");
    res = 0;
  } else if (IsOpenbsd()) {
    // TODO(jart): implement workaround for msyscall() dilemma
    dlerror_set("dlopen() isn't supported on OpenBSD yet");
    res = 0;
  } else if (foreign_init()) {
    res = __foreign.dlopen(path, mode);
  } else {
    res = 0;
  }
  ALLOW_CANCELATION;
  ALLOW_SIGNALS;
  STRACE("cosmo_dlopen(%#s, %d) → %p% m", path, mode, res);
  return res;
}

/**
 * Obtains address of symbol from dynamic shared object.
 *
 * WARNING: You almost always want to say this:
 *
 *     pFunction = cosmo_dltramp(cosmo_dlsym(dso, "function"));
 *
 * That will generate code at runtime for automatically translating to
 * Microsoft's x64 calling convention when appropriate. However the
 * automated solution doesn't always work. For example, the prototype:
 *
 *     void func(int, float);
 *
 * Won't be translated correctly, due to the differences in ABI. We're
 * able to smooth over most of them, but that's just one of several
 * examples where we can't. A good rule of thumb is:
 *
 *   - More than four float/double args is problematic
 *   - Having both integral and floating point parameters is bad
 *
 * For those kinds of functions, you need to translate the ABI by hand.
 * This can be accomplished using the GCC `__ms_abi__` attribute, where
 * you'd have two function pointer types branched upon `IsWindows()`.
 *
 * @param handle was opened by dlopen()
 * @return address of symbol, or NULL w/ dlerror()
 */
void *cosmo_dlsym(void *handle, const char *name) {
  void *func;
  if (IsWindows()) {
    func = dlsym_nt(handle, name);
  } else if (IsXnuSilicon()) {
    func = __syslib->__dlsym(handle, name);
  } else if (IsXnu()) {
    dlerror_set("dlopen() isn't supported on x86-64 MacOS");
    func = 0;
  } else if (foreign_init()) {
    func = __foreign.dlsym(handle, name);
  } else {
    func = 0;
  }
  STRACE("cosmo_dlsym(%p, %#s) → %p", handle, name, func);
  return func;
}

/**
 * Trampolines foreign function pointer so it can be called safely.
 */
void *cosmo_dltramp(void *foreign_func) {
  if (!IsWindows()) {
    return foreign_thunk_sysv(foreign_func);
  } else {
    return foreign_thunk_nt(foreign_func);
  }
}

/**
 * Closes dynamic shared object.
 *
 * @param handle was opened by dlopen()
 * @return 0 on success, or -1 w/ dlerror()
 */
int cosmo_dlclose(void *handle) {
  int res;
  if (IsWindows()) {
    res = dlclose_nt(handle);
  } else if (IsXnuSilicon()) {
    res = __syslib->__dlclose(handle);
  } else if (IsXnu()) {
    dlerror_set("dlopen() isn't supported on x86-64 MacOS");
    res = -1;
  } else if (foreign_init()) {
    res = __foreign.dlclose(handle);
  } else {
    res = -1;
  }
  STRACE("cosmo_dlclose(%p) → %d", handle, res);
  return res;
}

/**
 * Returns string describing last dlopen/dlsym/dlclose error.
 */
char *cosmo_dlerror(void) {
  char *res;
  if (IsXnuSilicon()) {
    res = __syslib->__dlerror();
  } else if (IsWindows() || IsXnu()) {
    res = dlerror_buf;
  } else if (foreign_init()) {
    res = __foreign.dlerror();
    res = dlerror_set(res);
  } else {
    res = dlerror_buf;
  }
  STRACE("cosmo_dlerror() → %#s", res);
  return res;
}
