/*
**  pth_mctx.c -- Pth machine context handling
**
**  Copyright (c) 1999 Ralf S. Engelschall <rse@engelschall.com>
**
**  This file is part of GNU Pth, a non-preemptive thread scheduling
**  library which can be found at http://www.gnu.org/software/pth/.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Library General Public
**  License as published by the Free Software Foundation; either
**  version 2 of the License, or (at your option) any later version.
**
**  This library 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
**  Library General Public License for more details.
**
**  You should have received a copy of the GNU Library General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
**  USA, or contact Ralf S. Engelschall <rse@engelschall.com>.
*/

#include "pth_p.h"

#if cpp

/* 
 * machine context state structure
 *
 * In `jb' the CPU registers, the program counter, the stack
 * pointer and (usually) the signals mask is stored. When the
 * signal mask cannot be implicitly stored in `jb', it's
 * alternatively stored explicitly in `sigs'. The `error' stores
 * the value of `errno'.
 */
typedef struct pth_mctx_st pth_mctx_t;
struct pth_mctx_st {
    pth_sigjmp_buf jb;
    sigset_t sigs;
#if defined(PTH_EMULATE_SIGSETJMP)
    sigset_t block;
#endif
    int error;
};

/*
** ____ MACHINE STATE SWITCHING ______________________________________
*/

/* 
 * save the current machine context
 */
#if defined(PTH_EMULATE_SIGSETJMP)
#define pth_mctx_save(mctx) \
        ( (mctx)->error = errno, \
          sigprocmask(SIG_SETMASK, &((mctx)->block), &((mctx)->sigs)), \
          pth_sigsetjmp((mctx)->jb) )
#else
#define pth_mctx_save(mctx) \
        ( (mctx)->error = errno, \
          sigprocmask(SIG_SETMASK, NULL, &((mctx)->sigs)), \
          pth_sigsetjmp((mctx)->jb) )
#endif

/* 
 * restore the current machine context
 * (at the location of the old context)
 */
#define pth_mctx_restore(mctx) \
        ( errno = (mctx)->error, \
          (void)pth_siglongjmp((mctx)->jb, 1) )

/* 
 * restore the current machine context 
 * (at the location of the new context)
 */
#if defined(PTH_EMULATE_SIGSETJMP)
#define pth_mctx_restored(mctx) \
        sigprocmask(SIG_SETMASK, &((mctx)->sigs), NULL)
#else
#define pth_mctx_restored(mctx) \
        /*nop*/
#endif

/* 
 * switch from one machine context to another 
 */
#define pth_mctx_switch(old,new) \
    pth_debug1("pth_mctx_switch"); \
    if (pth_mctx_save(old) == 0) \
        pth_mctx_restore(new); \
    pth_mctx_restored(old);

#endif /* cpp */

/*
** ____ MACHINE STATE INITIALIZATION ________________________________
*/

#if defined(PTH_MCTXSET_SIGSTACK)

/*
 * VARIANT 1: THE STANDARD
 *
 * This uses sigstack/sigaltstack() and friends and is really the
 * most tricky part of Pth. When you understand the following
 * stuff you're a good Unix hacker and then you've already
 * understood the gory ingredients of Pth.  So, either welcome to
 * the club of hackers, or do yourself a favor and skip this ;)
 *
 * The ingenious fact is that this variant runs really on _all_
 * POSIX compliant systems without special platform kludges. Not
 * even I've expect this, of course. But be _VERY_ carefully when
 * you change something in the following code. The slightest
 * change or reordering can lead to horribly broken code.  Really
 * every function call in the following case is intended to be
 * how it is, doubt me...
 */

#if !defined(SA_ONSTACK) && defined(SV_ONSTACK)
#define SA_ONSTACK SV_ONSTACK
#endif
#if !defined(SS_DISABLE) && defined(SA_DISABLE)
#define SS_DISABLE SA_DISABLE
#endif
#if defined(SA_DISABLE)
#define ss_sp ss_base
#endif

static pth_mctx_t            mctx_caller;
static volatile sig_atomic_t mctx_called;
static pth_mctx_t            mctx_trampoline;
static pth_mctx_t           *mctx_creating = NULL;
static void                (*mctx_creating_func)(void) = NULL;

/* trampoline jump-in function */
static void pth_mctx_set_trampoline_jumpin(void)
{
    pth_mctx_t *mctx_starting;
    void (*mctx_starting_func)(void);

    /*
     * Move startup details from static storage to local auto
     * variables which is necessary because it has to survive in
     * a local context until the thread is scheduled for real.
     */
    mctx_starting      = mctx_creating;
    mctx_starting_func = mctx_creating_func;
    mctx_creating      = NULL;
    mctx_creating_func = NULL;

    /* 
     * Save current machine state (on new stack) and 
     * go back to caller until we're scheduled for real...
     */
    pth_debug1("pth_mctx_set_trampoline_jumpin: switch back to caller");
    pth_mctx_switch(mctx_starting, &mctx_caller);

    /* 
     * The new thread is now running: GREAT!
     * Now we just invoke its init function....
     */
    pth_debug1("pth_mctx_set_trampoline_jumpin: reentered from scheduler");
    mctx_starting_func();
    abort();
}

/* trampoline signal handler */
static void pth_mctx_set_trampoline(int sig)
{
    sigset_t sigs;

    /* 
     * Unblock the SIGUSR1 signal that got us here because we've
     * to avoid that it's stored in blocked state in the machine
     * context.
     */
    sigemptyset(&sigs);
    sigaddset(&sigs, SIGUSR1);
    sigprocmask(SIG_UNBLOCK, &sigs, NULL);

    /* 
     * Save current machine state and _immediately_ go back with
     * a standard "return" (to stop the signal handler situation)
     * to let him remove the stack again. Notice that we really
     * have do a normal "return" here, or the OS would consider
     * the thread to be running on a signal stack which isn't
     * good (for instance it wouldn't allow us to spawn a thread
     * from within a thread, etc.)
     */
    if (pth_mctx_save(&mctx_trampoline) == 0) {
        pth_debug1("pth_mctx_set_trampoline: return to caller");
        mctx_called = TRUE;
        return;
    }
    pth_debug1("pth_mctx_set_trampoline: reentered from caller");

    /*
     * Ok, the caller has longjmp'ed back to us, so now prepare
     * us for the real machine state switching. We have to jump
     * into another function here to get a new stack context for
     * the auto variables (which have to be auto-variables
     * because the start of the thread happens later). Else with
     * PIC (i.e. Position Independent Code which is used when PTH
     * is built as a shared library) most platforms would
     * horrible core dump as experience showed.
     */
    pth_mctx_set_trampoline_jumpin();
}

/* initialize a machine state */
intern int pth_mctx_set(
    pth_mctx_t *mctx, void (*func)(void), char *sk_addr_lo, char *sk_addr_hi)
{
    struct sigaction sa;
    struct sigaction osa;
#if defined(HAVE_SIGALTSTACK)
#if defined(HAVE_STACK_T)
    stack_t ss;
#else
    struct sigaltstack ss;
#endif
#elif defined(HAVE_SIGSTACK)
    struct sigstack ss;
#endif
    sigset_t osigs;
    sigset_t sigs;

    pth_debug1("pth_mctx_set: enter");

    /* 
     * Preserve the SIGUSR1 signal state, block SIGUSR1, 
     * and establish our signal handler. The signal will
     * transfer control onto the signal stack.
     */
    sigemptyset(&sigs);
    sigaddset(&sigs, SIGUSR1);
    sigprocmask(SIG_BLOCK, &sigs, &osigs);
    memset((void *)&sa, 0, sizeof(struct sigaction));
    sa.sa_handler = pth_mctx_set_trampoline;
    sa.sa_flags = SA_ONSTACK;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGUSR1, &sa, &osa) != 0)
        return FALSE;
    
    /*
     * Set the new stack.
     *
     * For sigaltstack we're lucky [from sigaltstack(2) on
     * FreeBSD 3.1]: ``Signal stacks are automatically adjusted
     * for the direction of stack growth and alignment
     * requirements''
     * 
     * For sigstack we have to decide ourself [from sigstack(2)
     * on Solaris 2.6]: ``The direction of stack growth is not
     * indicated in the historical definition of struct sigstack.
     * The only way to portably establish a stack pointer is for
     * the application to determine stack growth direction.''
     */
#if defined(HAVE_SIGALTSTACK)
    ss.ss_sp    = sk_addr_lo; 
    ss.ss_size  = sk_addr_hi - sk_addr_lo;
    ss.ss_flags = 0;
    if (sigaltstack(&ss, NULL) < 0)
        return FALSE;
#elif defined(HAVE_SIGSTACK)
    ss.ss_sp = (PTH_STACKSGROWDOWN ? stack_hi_addr : stack_lo_addr);
    ss.ss_onstack = 0;
    if (sigstack(&ss, NULL) < 0)
        return FALSE;
#endif

    /* 
     * Tell startup code on signal stack where to dump 
     * the machine context, and what to do afterwards...
     */
    mctx_creating      = mctx;
    mctx_creating_func = func;

    /* 
     * Now transfer control onto the signal stack and set it up.
     * It will return immediately via "return" after the setjmp()
     * was performed. Be careful here with race conditions.  The
     * signal can be delivered the first time sigsuspend() is
     * called.
     */
    mctx_called = FALSE;
    kill(getpid(), SIGUSR1);
    sigfillset(&sigs);
    sigdelset(&sigs, SIGUSR1);
    while (!mctx_called)
        sigsuspend(&sigs);

    /* 
     * Inform the system that we are back off the signal stack by
     * removing the alternative signal stack. Be careful here: It
     * first has to be disabled, before it can be removed.
     */
#if defined(HAVE_SIGALTSTACK)
    sigaltstack(NULL, &ss);
    ss.ss_flags = SS_DISABLE;
    if (sigaltstack(&ss, NULL))
        return FALSE;
    sigaltstack(NULL, &ss);
    if (!(ss.ss_flags & SS_DISABLE)) 
        return_errno(FALSE, EIO);
#elif defined(HAVE_SIGSTACK)
    if (sigstack(&ss, NULL))
        return FALSE;
#endif

    /* 
     * Restore the old SIGUSR1 signal handler and mask
     */
    sigaction(SIGUSR1, &osa, NULL);
    sigprocmask(SIG_SETMASK, &osigs, NULL);

    /*
     * Initialize additional ingredients of the machine
     * context structure.
     */
#if defined(PTH_EMULATE_SIGSETJMP)
    sigemptyset(&mctx->block);
#endif
    sigemptyset(&mctx->sigs);
    mctx->error = 0;

    /* 
     * Now enter the trampoline again, but
     * this time not as a signal handler.
     * Instead we jump into it directly.
     */
    pth_mctx_switch(&mctx_caller, &mctx_trampoline);

    /*
     * Ok, we returned again, so now we're finished
     */
    pth_debug1("pth_mctx_set: leave");
    return TRUE;
}

#elif defined(PTH_MCTXSET_LINUX)

/*
 * VARIANT 2: LINUX SPECIFIC JMP_BUF FIDDLING
 *
 * Oh hell, I really love it when Linux guys talk about their
 * "POSIX compliant system". It's far away from POSIX compliant,
 * IMHO. Autoconf finds sigstack/sigaltstack() on Linux, yes.
 * But it doesn't work. Why? Because on Linux (even with glibc
 * 2.1!) these two functions are nothing more than silly stub
 * functions which always return just -1. Very useful, yeah...
 */

#include <features.h>

intern int pth_mctx_set(
    pth_mctx_t *mctx, void (*func)(void), char *sk_addr_lo, char *sk_addr_hi)
{
    pth_mctx_save(mctx);
#if defined(__GLIBC__) && defined(__GLIBC_MINOR__) \
    && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 0 && defined(JB_PC) && defined(JB_SP)
    mctx->jb[0].__jmpbuf[JB_PC] = (int)func;
    mctx->jb[0].__jmpbuf[JB_SP] = (int)sk_addr_hi;
#elif defined(__GNU_LIBRARY__) && defined(__i386__)
    mctx->jb[0].__jmpbuf[0].__pc = (char *)func;
    mctx->jb[0].__jmpbuf[0].__sp = sk_addr_hi;
#else
#error "Unknown Linux (g)libc version and/or platform"
#endif
#if defined(PTH_EMULATE_SIGSETJMP)
    sigemptyset(&mctx->block);
#endif
    sigemptyset(&mctx->sigs);
    mctx->error = 0;
    return TRUE;
}

/*
 * VARIANT X: JMP_BUF FIDDLING FOR ONE MORE ESOTERIC OS
 * Add the jmp_buf fiddling for your esoteric OS here...
 *
#elif defined(PTH_MCTX_ESOTERIC) 
intern int 
pth_mctx_set(pth_mctx_t *mctx, void (*func)(void), 
             char *sk_addr_lo, char *sk_addr_hi)
{
    pth_mctx_save(mctx);
#if defined(PTH_EMULATE_SIGSETJMP)
    sigemptyset(&mctx->block);
#endif
    sigemptyset(&mctx->sigs);
    mctx->error = 0;
    ...start hacking here...
    mctx->.... = func;
    mctx->.... = sk_addr_hi;
    mctx->.... = sk_addr_lo;
    return TRUE;
}
*/

#endif

