/*
 * Copyright 2018 Google, Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution;
 * neither the name of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors: Gabe Black
 */

#include "base/fiber.hh"

#if HAVE_VALGRIND
#include <valgrind/valgrind.h>
#endif

// Mac OS requires _DARWIN_C_SOURCE if _POSIX_C_SOURCE is defined,
// otherwise it will mask the definition of MAP_ANONYMOUS.
// _POSIX_C_SOURCE is already defined by including <ucontext.h> in
// base/fiber.hh
#if defined(__APPLE__) && defined(__MACH__)
#define _DARWIN_C_SOURCE
#endif

#include <sys/mman.h>
#include <unistd.h>

#include <cerrno>
#include <cstdio>
#include <cstring>

#include "base/logging.hh"

using namespace std;

namespace
{

/*
 * The PrimaryFiber class is a special case that attaches to the currently
 * executing context. That makes handling the "primary" fiber, aka the one
 * which most of gem5 is running under, no different than other Fibers.
 */
class PrimaryFiber : public Fiber
{
  public:
    PrimaryFiber() : Fiber(nullptr, 0) { setStarted(); }
    void main() { panic("PrimaryFiber main executed.\n"); }
};

PrimaryFiber _primaryFiber;

// A pointer to whatever the currently executing Fiber is.
Fiber *_currentFiber = &_primaryFiber;

// A pointer to the Fiber which is currently being started/initialized.
Fiber *startingFiber = nullptr;

} // anonymous namespace

void
Fiber::entryTrampoline()
{
    startingFiber->start();
}

Fiber::Fiber(size_t stack_size) : Fiber(primaryFiber(), stack_size)
{}

Fiber::Fiber(Fiber *link, size_t stack_size) :
    link(link), stack(nullptr), stackSize(stack_size), guardPage(nullptr),
    guardPageSize(sysconf(_SC_PAGE_SIZE)), started(false), _finished(false)
{
    if (stack_size) {
        guardPage = mmap(nullptr, guardPageSize + stack_size,
                         PROT_READ | PROT_WRITE,
                         MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
        if (guardPage == (void *)MAP_FAILED) {
            perror("mmap");
            fatal("Could not mmap %d byte fiber stack.\n", stack_size);
        }
        stack = (void *)((uint8_t *)guardPage + guardPageSize);
        if (mprotect(guardPage, guardPageSize, PROT_NONE)) {
            perror("mprotect");
            fatal("Could not forbid access to fiber stack guard page.");
        }
    }
#if HAVE_VALGRIND
    valgrindStackId = VALGRIND_STACK_REGISTER(
            stack, (uint8_t *)stack + stack_size);
#endif
}

Fiber::~Fiber()
{
    panic_if(stack && _currentFiber == this, "Fiber stack is in use.");
#if HAVE_VALGRIND
    VALGRIND_STACK_DEREGISTER(valgrindStackId);
#endif
    if (guardPage)
        munmap(guardPage, guardPageSize + stackSize);
}

void
Fiber::createContext()
{
    // Set up a context for the new fiber, starting it in the trampoline.
    getcontext(&ctx);
    ctx.uc_stack.ss_sp = stack;
    ctx.uc_stack.ss_size = stackSize;
    ctx.uc_link = nullptr;
    makecontext(&ctx, &entryTrampoline, 0);

    // Swap to the new context so it can enter its start() function. It
    // will then swap itself back out and return here.
    startingFiber = this;
    panic_if(!_currentFiber, "No active Fiber object.");
    swapcontext(&_currentFiber->ctx, &ctx);

    // The new context is now ready and about to call main().
}

void
Fiber::start()
{
    // Avoid a dangling pointer.
    startingFiber = nullptr;

    setStarted();

    // Swap back to the parent context which is still considered "current",
    // now that we're ready to go.
    int ret M5_VAR_USED = swapcontext(&ctx, &_currentFiber->ctx);
    panic_if(ret == -1, strerror(errno));

    // Call main() when we're been reactivated for the first time.
    main();

    // main has returned, so this Fiber has finished. Switch to the "link"
    // Fiber.
    _finished = true;
    link->run();
}

void
Fiber::run()
{
    panic_if(_finished, "Fiber has already run to completion.");

    // If we're already running this fiber, we're done.
    if (_currentFiber == this)
        return;

    if (!started)
        createContext();

    // Switch out of the current Fiber's context and this one's in.
    Fiber *prev = _currentFiber;
    Fiber *next = this;
    _currentFiber = next;
    swapcontext(&prev->ctx, &next->ctx);
}

Fiber *Fiber::currentFiber() { return _currentFiber; }
Fiber *Fiber::primaryFiber() { return &_primaryFiber; }