rustfs-mimalloc-sys 0.1.0

Low-level FFI bindings to mimalloc V3
Documentation
/* ----------------------------------------------------------------------------
Copyright (c) 2019-2026, Microsoft Research, Daan Leijen
This is free software; you can redistribute it and/or modify it under the
terms of the MIT license. A copy of the license can be found in the file
"LICENSE" at the root of this distribution.
-----------------------------------------------------------------------------*/

/* ----------------------------------------------------------------------------
  Implement dynamic thread local variables (used by heap's for their theap's).
  Unlike most OS native implementations there is no limit on the number
  that can be allocated.
-----------------------------------------------------------------------------*/

#include "mimalloc.h"
#include "mimalloc/internal.h"
#include "mimalloc/prim.h"

/* -----------------------------------------------------------
  Each thread can have (a dynamically expanding) array of
  thread-local values. Each slot has a value and a version.
  The version is used to safely reuse slots.
----------------------------------------------------------- */
typedef struct mi_tls_slot_s {
  size_t  version;
  void*   value;
} mi_tls_slot_t;

typedef struct mi_thread_locals_s {
  size_t        count;
  mi_memid_t    memid;
  mi_tls_slot_t slots[1];
} mi_thread_locals_t;

static mi_thread_locals_t mi_thread_locals_empty = mi_init_struct_zero;


/* -----------------------------------------------------------
  We have 2 thread local variable which we implement with either
  a C thread local declaration or using pthread keys.
  - mi_thread_locals: points to an array of thread locals for most keys
  - mi_slot_fast: a single dedicated thread local for slightly faster access. (used for the main heap's theap)
----------------------------------------------------------- */

#if MI_TLS_MODEL_PTHREADS || defined(__APPLE__)   // macOS has fast pthreads
// Use pthreads
#define mi_define_thread_local(tp,name,initval) \
  static pthread_key_t __##name##_key = MI_PTHREAD_KEY_INVALID; \
  static inline tp   name##_peek(void)    { return (tp)mi_pthread_key_get(__##name##_key); } \
  static inline tp   name##_get(void)     { tp result = name##_peek(); return (result!=NULL ? result : initval); } \
  static inline bool name##_set(tp val)   { return mi_pthread_key_set(&__##name##_key,val); } \
  static inline void name##_delete(void)  { mi_pthread_key_delete(&__##name##_key); }

#else
// Direct thread locals
#define mi_define_thread_local(tp,name,initval) \
  static mi_decl_thread tp __##name = initval; \
  static inline tp   name##_peek(void)    { return __##name; } \
  static inline tp   name##_get(void)     { tp result = __##name; return (result!=NULL ? result : initval); } \
  static inline bool name##_set(tp val)   { __##name = val; return true; } \
  static inline void name##_delete(void)  {  }
#endif

mi_define_thread_local(mi_thread_locals_t*, mi_thread_locals, &mi_thread_locals_empty)
mi_define_thread_local(void*, mi_slot_fast, NULL)


/* -----------------------------------------------------------
  Each key consists of the slot index in the lower bits,
  and its version it the top bits. When we get a value
  the version must match or we return NULL. When we set
  a value, we also set the version of the key.
----------------------------------------------------------- */

#if MI_SIZE_BITS >= 64
#define MI_TLS_IDX_BITS     (MI_SIZE_BITS/4)      /* 16 bits for the index, 48 bits for the version */
#elif MI_SIZE_BITS >= 32
#define MI_TLS_IDX_BITS     (12)                  /* 12 bits for index, 20 for the version? */
#else
#error not enough bits for the version for thread locals
#endif
#define MI_TLS_IDX_MASK     ((MI_ZU(1)<<MI_TLS_IDX_BITS)-1)
#define MI_TLS_IDX_MAX      MI_TLS_IDX_MASK
#define MI_TLS_VERSION_MAX  ((MI_ZU(1)<<(MI_SIZE_BITS - MI_TLS_IDX_BITS))-1)


static size_t mi_key_index( size_t key ) {
  return (key & MI_TLS_IDX_MASK);
}

static size_t mi_key_version( size_t key ) {
  return (key >> MI_TLS_IDX_BITS);
}

static mi_thread_local_t mi_key_create( size_t index, size_t version ) {
  mi_assert_internal(version != 0 && version <= MI_TLS_VERSION_MAX);
  mi_assert_internal(index <= MI_TLS_IDX_MAX);
  const mi_thread_local_t key = ((version << MI_TLS_IDX_BITS) | index);
  mi_assert_internal(key != 0);
  return key;
}


// dynamically reallocate the thread local slots when needed
static mi_thread_locals_t* mi_thread_locals_expand(size_t least_idx) {
  mi_thread_locals_t* tls_old = mi_thread_locals_get();
  const size_t count_old = tls_old->count;
  size_t count;
  if (count_old==0) {
    tls_old = NULL; // so we allocate fresh from mi_thread_locals_empty
    count = 16;     // start with 16 slots
  }
  else if (count_old >= 1024) {
    count = count_old + 1024;  // at some point increase linearly
  }
  else {
    count = 2*count_old;       // and double initially
  }
  if (count <= least_idx) {
    count = least_idx + 1;
  }
  if (count > MI_TLS_IDX_MAX) { return NULL; }  // too large
  // allocate as meta (for secure mode)
  // we could also allocate on the main heap; this is recursion safe as that uses the fast local key
  mi_memid_t memid = (tls_old==NULL ? _mi_memid_none() : tls_old->memid);
  mi_thread_locals_t* tls = (mi_thread_locals_t*)_mi_meta_rezalloc(_mi_subproc(), tls_old, sizeof(mi_thread_locals_t) + count*sizeof(mi_tls_slot_t), &memid);
  if mi_unlikely(tls==NULL) return NULL;
  tls->memid = memid;
  tls->count = count;
  mi_thread_locals_set(tls);
  return tls;
}

static mi_decl_noinline bool mi_thread_local_set_expand( mi_thread_local_t key, void* val ) {
  if (val==NULL) return true;
  const size_t idx = mi_key_index(key);
  mi_thread_locals_t* tls = mi_thread_locals_expand(idx);
  if (tls==NULL) {
    _mi_error_message(EFAULT,"unable to allocate thread local variables\n");
    return false;
  }
  mi_assert_internal(tls == mi_thread_locals_get());
  mi_assert_internal(idx < tls->count);
  tls->slots[idx].value = val;
  tls->slots[idx].version = mi_key_version(key);
  return true;
}

// set a tls slot; returns `true` if successful.
// Can return `false` if we could not reallocate the slots array.
static mi_decl_noinline bool mi_thread_local_set_regular( mi_thread_local_t key, void* val ) {
  mi_thread_locals_t* tls = mi_thread_locals_get();
  mi_assert_internal(tls!=NULL);
  mi_assert_internal(key!=0);
  const size_t idx = mi_key_index(key);
  if mi_likely(idx < tls->count) {
    tls->slots[idx].value = val;
    tls->slots[idx].version = mi_key_version(key);
    return true;
  }
  else {
    return mi_thread_local_set_expand( key, val );  // tailcall
  }
}

bool _mi_thread_local_set( mi_thread_local_t key, void* val ) {
  mi_assert_internal(key!=0);
  if (key == mi_thread_local_key_fast) {
    return mi_slot_fast_set(val);
  }
  else {
    return mi_thread_local_set_regular(key,val);
  }
}

// get a tls slot value
static mi_decl_noinline void* mi_thread_local_get_regular( mi_thread_local_t key ) {
  mi_assert_internal(key!=0);
  const mi_thread_locals_t* const tls = mi_thread_locals_peek();
  if mi_unlikely(tls==NULL) {
    // this can happen if a thread local is accessed after the thread local has been freed
    // from mi_thread_done or mi_process_done.
    // todo: can we remove this check? now we can still call this from process done when stats are printed (which calls mi_heap_theap_peek)
    return NULL;
  }
  const size_t idx = mi_key_index(key);
  if mi_likely(idx < tls->count && mi_key_version(key) == tls->slots[idx].version) {
    return tls->slots[idx].value;
  }
  else {
    return NULL;
  }
}

// get a thread local value
void* _mi_thread_local_get( mi_thread_local_t key ) {
  mi_assert_internal(key!=0);
  if mi_likely(key == mi_thread_local_key_fast) {
    return mi_slot_fast_get();
  }
  else {
    return mi_thread_local_get_regular(key);
  }
}

void _mi_thread_locals_thread_done(void) {
  mi_thread_locals_t* const tls = mi_thread_locals_peek();
  if (tls!=NULL && tls->count > 0) {
    _mi_meta_free(_mi_subproc(), tls, tls->memid);
    mi_thread_locals_set(NULL);
  }
  if (mi_slot_fast_peek() != NULL) {
    mi_slot_fast_set(NULL);
  }
}

/* -----------------------------------------------------------
Create and free fresh TLS key's
----------------------------------------------------------- */
#include "bitmap.h"

static mi_lock_t    mi_thread_locals_lock;    // we need a lock in order to re-allocate the slot bits
static mi_bitmap_t* mi_thread_locals_free;    // reuse an arena bitmap to track which slots were assigned (1=free, 0=in-use)
static mi_memid_t   mi_thread_locals_memid;   // provenance of mi_thread_locals_free
static size_t       mi_thread_locals_version; // version to be able to reuse slots safely

void _mi_thread_locals_init(void) {
  mi_lock_init(&mi_thread_locals_lock);
}

void _mi_thread_locals_done(void) {
  mi_lock(&mi_thread_locals_lock) {
    mi_bitmap_t* const slots = mi_thread_locals_free;
    if (slots!=NULL) {
      _mi_meta_free(_mi_subproc_main(), slots, mi_thread_locals_memid);
    }
  }
  mi_lock_done(&mi_thread_locals_lock);
  mi_thread_locals_delete();
  mi_slot_fast_delete();
}

// strange signature but allows us to reuse the arena code for claiming free pages
static bool mi_thread_local_claim_fun(size_t _slice_index, mi_arena_t* _arena, bool* keep_set) {
  MI_UNUSED(_slice_index); MI_UNUSED(_arena);
  *keep_set = false;
  return true;
}

// When we claim a free slot, we increase the global version counter
// (so if we reuse a slot it will be returning NULL initially when a thread tries to get it)
static mi_thread_local_t mi_thread_local_claim(void) {
  size_t idx = 0;
  if (mi_thread_locals_free != NULL && mi_bitmap_try_find_and_claim(mi_thread_locals_free,0,&idx,&mi_thread_local_claim_fun,NULL)) {
    mi_thread_locals_version++;
    if (mi_thread_locals_version >= MI_TLS_VERSION_MAX) { mi_thread_locals_version = 1; }  /* wrap around the version */
    return mi_key_create( idx, mi_thread_locals_version);
  }
  else {
    return 0;
  }
}

static bool mi_thread_local_create_expand(void) {
  mi_bitmap_t* const slots = mi_thread_locals_free;
  // 1024 bits at a time
  const size_t oldcount = (slots==NULL ? 0 : mi_bitmap_max_bits(slots));
  const size_t newcount = 1024 + oldcount;
  if (newcount > MI_TLS_IDX_MAX) { return false; }
  const size_t newsize = mi_bitmap_size( newcount, NULL );
  // mi_bitmap_t* newslots = (mi_bitmap_t*)mi_zalloc_aligned(newsize, MI_BCHUNK_SIZE);
  mi_memid_t memid;
  mi_bitmap_t* newslots = (mi_bitmap_t*)_mi_meta_zalloc_aligned(_mi_subproc_main(), newsize, MI_BCHUNK_SIZE, &memid); // always allocate thread locals in the main subprocess
  mi_assert_internal(_mi_is_aligned(newslots,MI_BCHUNK_SIZE));
  if (newslots==NULL) { return false; }
  if (slots!=NULL) {
    // copy over the previous bitmap
    const size_t oldsize = mi_bitmap_size(oldcount,NULL);
    _mi_memcpy_aligned(newslots, slots, oldsize);
    _mi_meta_free(_mi_subproc_main(), slots, mi_thread_locals_memid);
  }
  mi_bitmap_init(newslots, newcount, true /* pretend already zero'd so we do not zero out the copied old entries */);
  mi_bitmap_unsafe_setN(newslots, oldcount, newcount - oldcount);  /* set the new expanded slots as available */
  mi_thread_locals_free = newslots;
  mi_thread_locals_memid = memid;
  return true;
}


// create a fresh key
mi_thread_local_t _mi_thread_local_create(void) {
  mi_thread_local_t key = 0;
  mi_lock(&mi_thread_locals_lock) {
    key = mi_thread_local_claim();
    if (key==0) {
      if (mi_thread_local_create_expand()) {
        key = mi_thread_local_claim();
      }
    }
  }
  mi_assert_internal(key!=0);
  mi_assert_internal(key!=mi_thread_local_key_fast);
  return key;
}

// free a key
void _mi_thread_local_free(mi_thread_local_t key) {
  if (key==0) return;
  const size_t idx = mi_key_index(key);
  mi_lock(&mi_thread_locals_lock) {
    mi_bitmap_t* const slots = mi_thread_locals_free;
    if (slots!=NULL && idx < mi_bitmap_max_bits(slots)) {
      mi_bitmap_set(slots,idx);
    }
  }
}