// GNU D Compiler emulated TLS routines.
// Copyright (C) 2019-2024 Free Software Foundation, Inc.

// GCC is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3, or (at your option) any later
// version.

// GCC is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
// for more details.

// Under Section 7 of GPL version 3, you are granted additional
// permissions described in the GCC Runtime Library Exception, version
// 3.1, as published by the Free Software Foundation.

// You should have received a copy of the GNU General Public License and
// a copy of the GCC Runtime Library Exception along with this program;
// see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
// <http://www.gnu.org/licenses/>.

// This code is based on the libgcc emutls.c emulated TLS support.

module gcc.emutls;

import core.atomic, core.stdc.stdlib, core.stdc.string, core.sync.mutex;
import core.internal.container.array;
import core.internal.container.hashtab;
import core.internal.traits : classInstanceAlignment;
import gcc.builtins, gcc.gthread;

version (GNU_EMUTLS): private:

alias word = __builtin_machine_uint;
alias pointer = __builtin_pointer_uint;
alias TlsArray = Array!(void**);

/*
 * TLS control data emitted by GCC for every TLS variable.
 */
struct __emutls_object
{
    word size;
    word align_;
    union
    {
        pointer offset;
        void* ptr;
    }

    ubyte* templ;
}

// Per-thread key to obtain the per-thread TLS variable array
__gshared __gthread_key_t emutlsKey;
// Largest, currently assigned TLS variable offset
__gshared pointer emutlsMaxOffset = 0;
// Contains the size of the TLS variables (for GC)
__gshared Array!word emutlsSizes;
// Contains the TLS variable array for single-threaded apps
__gshared TlsArray singleArray;
// List of all currently alive TlsArrays (for GC)
__gshared HashTab!(TlsArray*, TlsArray*) emutlsArrays;

// emutlsMutex Mutex + @nogc handling
enum mutexAlign = classInstanceAlignment!Mutex;
enum mutexClassInstanceSize = __traits(classInstanceSize, Mutex);
__gshared align(mutexAlign) void[mutexClassInstanceSize] _emutlsMutex;

@property Mutex emutlsMutex() nothrow @nogc
{
    return cast(Mutex) _emutlsMutex.ptr;
}

/*
 * Global (de)initialization functions
 */
extern (C) void _d_emutls_init() nothrow @nogc
{
    memcpy(_emutlsMutex.ptr, typeid(Mutex).initializer.ptr, _emutlsMutex.length);
    (cast(Mutex) _emutlsMutex.ptr).__ctor();

    if (__gthread_key_create(&emutlsKey, &emutlsDestroyThread) != 0)
        abort();
}

__gshared __gthread_once_t initOnce = GTHREAD_ONCE_INIT;

/*
 * emutls main entrypoint, called by GCC for each TLS variable access.
 */
extern (C) void* __emutls_get_address(shared __emutls_object* obj) nothrow @nogc
{
    pointer offset;
    if (__gthread_active_p())
    {
        // Obtain the offset index into the TLS array (same for all-threads)
        // for requested var. If it is unset, obtain a new offset index.
        offset = atomicLoad!(MemoryOrder.acq, pointer)(obj.offset);
        if (__builtin_expect(offset == 0, 0))
        {
            __gthread_once(&initOnce, &_d_emutls_init);
            emutlsMutex.lock_nothrow();

            offset = obj.offset;
            if (offset == 0)
            {
                offset = ++emutlsMaxOffset;

                emutlsSizes.ensureLength(offset);
                // Note: it's important that we copy any data from obj and
                // do not keep an reference to obj itself: If a library is
                // unloaded, its tls variables are not removed from the arrays
                // and the GC will still scan these. If we then try to reference
                // a pointer to the data segment of an unloaded library, this
                // will crash.
                emutlsSizes[offset - 1] = obj.size;

                atomicStore!(MemoryOrder.rel, pointer)(obj.offset, offset);
            }
            emutlsMutex.unlock_nothrow();
        }
    }
    // For single-threaded systems, don't synchronize
    else
    {
        if (__builtin_expect(obj.offset == 0, 0))
        {
            offset = ++emutlsMaxOffset;

            emutlsSizes.ensureLength(offset);
            emutlsSizes[offset - 1] = obj.size;

            obj.offset = offset;
        }
    }

    TlsArray* arr;
    if (__gthread_active_p())
        arr = cast(TlsArray*) __gthread_getspecific(emutlsKey);
    else
        arr = &singleArray;

    // This will always be false for singleArray
    if (__builtin_expect(arr == null, 0))
    {
        arr = mallocTlsArray(offset);
        __gthread_setspecific(emutlsKey, arr);
        emutlsMutex.lock_nothrow();
        emutlsArrays[arr] = arr;
        emutlsMutex.unlock_nothrow();
    }
    // Check if we have to grow the per-thread array
    else if (__builtin_expect(offset > arr.length, 0))
    {
        (*arr).ensureLength(offset);
    }

    // Offset 0 is used as a not-initialized marker above. In the
    // TLS array, we start at 0.
    auto index = offset - 1;

    // Get the per-thread pointer from the TLS array
    void** ret = (*arr)[index];
    if (__builtin_expect(ret == null, 0))
    {
        // Initial access, have to allocate the storage
        ret = emutlsAlloc(obj);
        (*arr)[index] = ret;
    }

    return ret;
}

// 1:1 copy from libgcc emutls.c
extern (C) void __emutls_register_common(__emutls_object* obj, word size, word align_, ubyte* templ) nothrow @nogc
{
    if (obj.size < size)
    {
        obj.size = size;
        obj.templ = null;
    }
    if (obj.align_ < align_)
        obj.align_ = align_;
    if (templ && size == obj.size)
        obj.templ = templ;
}

// 1:1 copy from libgcc emutls.c
void** emutlsAlloc(shared __emutls_object* obj) nothrow @nogc
{
    void* ptr;
    void* ret;
    enum pointerSize = (void*).sizeof;

    /* We could use here posix_memalign if available and adjust
     emutls_destroy accordingly.  */
    if ((cast() obj).align_ <= pointerSize)
    {
        ptr = malloc((cast() obj).size + pointerSize);
        if (ptr == null)
            abort();
        (cast(void**) ptr)[0] = ptr;
        ret = ptr + pointerSize;
    }
    else
    {
        ptr = malloc(obj.size + pointerSize + obj.align_ - 1);
        if (ptr == null)
            abort();
        ret = cast(void*)((cast(pointer)(ptr + pointerSize + obj.align_ - 1)) & ~cast(
                pointer)(obj.align_ - 1));
        (cast(void**) ret)[-1] = ptr;
    }

    if (obj.templ)
        memcpy(ret, cast(ubyte*) obj.templ, cast() obj.size);
    else
        memset(ret, 0, cast() obj.size);

    return cast(void**) ret;
}

/*
 * When a thread has finished, free all allocated TLS variables and empty the
 * array.  The pointer is not free'd as it is stil referenced by the GC scan
 * list emutlsArrays, which gets destroyed when druntime is unloaded.
 */
extern (C) void emutlsDestroyThread(void* ptr) nothrow @nogc
{
    auto arr = cast(TlsArray*) ptr;

    foreach (entry; *arr)
    {
        if (entry)
            free(entry[-1]);
    }

    arr.length = 0;
}

/*
 * Allocate a new TLS array, set length according to offset.
 */
TlsArray* mallocTlsArray(pointer offset = 0) nothrow @nogc
{
    static assert(TlsArray.alignof == (void*).alignof);
    void[] data = malloc(TlsArray.sizeof)[0 .. TlsArray.sizeof];
    if (data.ptr == null)
        abort();

    static immutable TlsArray init = TlsArray.init;
    memcpy(data.ptr, &init, data.length);
    (cast(TlsArray*) data).length = 32;
    return cast(TlsArray*) data.ptr;
}

/*
 * Make sure array is large enough to hold an entry for offset.
 * Note: the array index will be offset - 1!
 */
void ensureLength(Value)(ref Array!(Value) arr, size_t offset) nothrow @nogc
{
    // index is offset-1
    if (offset > arr.length)
    {
        auto newSize = arr.length * 2;
        if (offset > newSize)
            newSize = offset + 32;
        arr.length = newSize;
    }
}

// Public interface
public:
void _d_emutls_scan(scope void delegate(void* pbeg, void* pend) nothrow cb) nothrow
{
    void scanArray(scope TlsArray* arr) nothrow
    {
        foreach (index, entry; *arr)
        {
            auto ptr = cast(void*) entry;
            if (ptr)
                cb(ptr, ptr + emutlsSizes[index]);
        }
    }

    __gthread_once(&initOnce, &_d_emutls_init);
    emutlsMutex.lock_nothrow();
    // this code is effectively nothrow
    try
    {
        foreach (arr, value; emutlsArrays)
        {
            scanArray(arr);
        }
    }
    catch (Exception)
    {
    }
    emutlsMutex.unlock_nothrow();
    scanArray(&singleArray);
}

// Call this after druntime has been unloaded
void _d_emutls_destroy() nothrow @nogc
{
    (cast(Mutex) _emutlsMutex.ptr).__dtor();
    destroy(emutlsArrays);
}
