#!/bin/sh

# relaytool 1.4
# (c) 2004 Mike Hearn <mike@navi.cx>
#
# This is a program to provide a more convenient interface to dlopen/dlsym.
#
# It lets you write the same style of code you would when using a normal hard
# link (-lwhatever), but the symbols are actually lazy-linked at runtime. You can
# use the symbols libwhatever_is_present and libwhatever_symbol_is_present()
# to find out what APIs are actually available at runtime. In other words,
# the need to use function pointers and lots of manual calls to dlsym() is
# eliminated, and it becomes much simpler to soft link to things as a result.
#
# If a symbol is missing at runtime and you call it anyway, your application
# will abort and an error message is printed that states which function was
# called. If a variable is missing at runtime, the value is always -1.
#
# See below for usage instructions.
#
#############################################################################
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#################################################################################
#
# Usage:
#
# Replace -lfoo in your link line with `relaytool --relay foo -lfoo`, assuming libfoo is
# in the standard search path (/lib/, /usr/lib, /usr/local/lib). You can pass
# entire gcc linker lines to relaytool, and it will interpret them accordingly. Most
# options will be passed through unchanged onto stdout, but -lfoo options that have a
# preceeding "--relay foo" argument will trigger stub generation and compilation.
#
# Note that relaytool will invoke $CC (or gcc if it's not set) to compile the stub file
# silently. You will not see any output.
#
# Because relaytool parses linker options, you can feed the output of pkg-config and other
# config scripts to it:
#
#    relaytool --relay gtkspell `pkg-config --libs gtkspell-2.0`
#
# produces
#
#    -Wl,--export-dynamic -L/opt/gnome26/lib libgtkspell.so.0.0.0.stub.o -laspell -lgtk-x11-2.0
#    -lgdk-x11-2.0 -latk-1.0 -lgdk_pixbuf-2.0 -lm -lpangoxft-1.0 -lpangox-1.0 -lpango-1.0
#    -lgobject-2.0 -lgmodule-2.0 -ldl -lglib-2.0
#
# on my system. Note how -lgtkspell was replaced with libgtkspell.so.0.0.0.stub.o
#
# On architectures that relaytool does not support, the -l is passed through along
# with a couple of defines to provide libfoo_is_present and libfoo_symbol_is_present.
#
# Warning: ensure CFLAGS="-fPIC" if the library exports variables, as
# GOT fixup requires your program to use PIC code.
#
# If you want to use relaytool only for part of a shared library (for
# instance to make use of symbols available in newer versions only)
# you can use the --partial-map feature. To use this create a file
# with a line for each symbol, with F for function or V for variable
# in the first column, like so:
#
#   foo.relaymap
#
#   F some_function
#   V some_variable
#
# then run: relaytool --partial-map foo.relaymap /usr/lib/libwhatever.so.0
#
#################################################################################
#
# TODO:
#   - Using asm statements probably disables emission of PT_GNU_STACK, figure out
#     how to re-enable that
#   - Finish PPC port

using_partial_map=false

if [[ "$@" == "" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
    echo "relaytool: relaytool --relay foo -L/some/path -lfoo"
    echo
    echo "Will generate a file that can be used instead of linking"
    echo "directly against a library, which will dlopen the DSO at init time"
    echo ""
    echo "Usage:"
    echo "  Pass a standard set of link commands as parameters. Each -lfoo"
    echo "  options which matches a corresponding --relay parameter will be replaced"
    echo "  with the name of a generated and compiled file. The rest will be echoed"
    echo "  on stdout unchanged."
    exit 1
fi

function error() {
    echo $@ >/dev/stderr
    exit 1
}

function relay() {
    lib=$1
    libname=$( echo $( basename $lib ) | sed 's/\.so.*//' | sed 's/\./_/g' )
    outfile="`basename $lib`.stub.c"

    echo -n $outfile

    if $using_partial_map; then
        functions=$( grep "^F " $partial_map | cut -d' ' -f2 )
        variables=$( grep "^V " $partial_map | cut -d' ' -f2 )
    else
        functions=$( nm -D "$lib" | awk '{ if ($2 == "T") print $3; }' | LC_ALL=C grep -v '\(_init\|_fini\)' )
        variables=$( nm -D "$lib" | awk '{ if (($2 == "D") || ($2 == "G")) print $3; }' )
    fi

    cat <<EOF >"$outfile"
/* automatically generated: `date` by `id -un`@`uname -n`, do not edit
 *
 * Built by relaytool, a program for building delay-load jumptables
 * relaytool is (C) 2004 Mike Hearn <mike@navi.cx>
 * See http://autopackage.org/ for details.
 */
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/mman.h>

#ifdef __cplusplus
    extern "C" {
#endif

static void **ptrs;
static char *functions[] = {
EOF

    for s in $functions; do
        echo "    \"$s\"," >>$outfile
    done
    echo "    0" >>$outfile

    cat <<EOF >>$outfile
};

static char *variables[] = {
EOF

    for s in $variables; do
        echo "    \"$s\"," >>$outfile
    done
    echo "    0" >>$outfile

    cat <<EOF >>$outfile
};

/* 1 if present, 0 if not */
int ${libname}_is_present = 0;

static void *handle = 0;

/* 1 if present, 0 if not, 0 with warning to stderr if lib not present or symbol not found */
int ${libname}_symbol_is_present(char *s) {
    int i;

    if( !${libname}_is_present ) {
        fprintf(stderr, "%s: relaytool: `basename $lib` not present so cannot check for symbol %s.\n", getenv("_"), s);
        fprintf(stderr, "%s: relaytool: This probably indicates a bug in the application, please report.\n", getenv("_"));
        return 0;
    }

    i = 0;
    while (functions[i++]) if (!strcmp( functions[i - 1], s )) return ptrs[i - 1] > 0 ? 1 : 0;
    i = 0;
    while (variables[i++]) if (!strcmp( variables[i - 1], s )) return dlsym( handle, s ) > 0 ? 1 : 0;

    fprintf( stderr, "%s: relaytool: %s is an unknown symbol in `basename $lib`.\n", getenv("_"), s );
    fprintf( stderr, "%s: relaytool: If you are the developer of this program, please correct the symbol name or rerun relaytool.\n", getenv("_") );
    return 0;
}

static __attribute__((noreturn)) void _relaytool_stubcall_${libname}(int offset) {
    fprintf( stderr, "%s: relaytool: stub call to `basename ${lib}`:%s, aborting.\n", getenv("_"),
             functions[offset / sizeof(void*)] );
    exit( 1 );
}

#if defined( __i386__ )
    #define FIXUP_GOT_RELOC(sym, addr) \\
        asm("\tmovl %0, %%eax\n" \\
            "\tmovl %%eax, " sym "@GOT(%%ebx)\n" : : "r" (addr));
#elif defined( __powerpc__ )
    #define FIXUP_GOT_RELOC(sym, addr) \\
        asm("\tlis r11, %0\n"
            "\tstw " sym "@GOT(r11)\n"
        );
#else        
    #error Please define FIXUP_GOT_RELOC for your architecture
#endif

static __attribute__((constructor)) void _relaytool_init_${libname}() {
    int i = 0;

    handle = dlopen( "`basename $lib`", RTLD_LAZY );
    
    if (handle) ${libname}_is_present = 1;

    (void*) ptrs = mmap( NULL,  sizeof(functions), PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_PRIVATE | MAP_ANONYMOUS, 0, 0 );

    assert( ptrs != MAP_FAILED );
    
    /* build function jumptable */
    while (functions[i++]) if (handle) ptrs[i - 1] = dlsym( handle, functions[i - 1] );
    
EOF

    if [[ "$variables" != "" ]]; then echo "    /* now fixup the global offset table for variable imports */" >>$outfile; fi
    for s in $variables; do
        echo "    FIXUP_GOT_RELOC( \"$s\", dlsym(handle, \"$s\") );" >>$outfile
    done

    cat <<EOF >>$outfile
}

#if defined( __i386__ )

#define JUMP_SLOT(name, index)                          \\
    asm(".global   " name "\n"                          \\
        "        .type " name ", @function\n"           \\
        name ":\n"                                      \\
        "        movl ptrs, %eax\n"                     \\
        "        movl " #index "(%eax), %eax\n"         \\
        "        test %eax, %eax\n"                     \\
        "        jnz  JS" #index "\n"                   \\
        "        push \$" #index "\n"                   \\
        "        call _relaytool_stubcall_${libname}\n" \\
        "JS" #index ":    jmp *%eax\n");

#elif defined( __powerpc__ )

#define JUMP_SLOT(name, index) \\
    asm(".global    " name "\n" \\
        "        .type " name ", @function\n" \\
        name ":\n" \\
        "        lis r11, ptrs@ha\n" \\
        "        lwz r11, " #index "(r11)\n" \\
        "        cmpi cr0,r11,0\n" \\
        "        beq- 1f\n" \\
        "        mtctr r11\n" \\
        "        bctr\n"
        "1:      li r3, " #index "\n" \\
        "        b _relaytool_stubcall_${libname}\n"
        );
        
#else        
    #error Please define JUMP_SLOT for your architecture
#endif

/* define placeholders for the variable imports: their type doesn't matter,
   however we must restrict ELF symbol scope to prevent the definition in the imported
   shared library being bound to this dummy symbol (not all libs are compiled -Bsymbolic)
 */
EOF

    for s in $variables; do
        echo "int $s __attribute__(( visibility(\"hidden\") )) = -1;" >>$outfile
    done

    cat <<EOF >>"$outfile"

/* switch to the code section and define jumpslots */
asm(".text\n");

EOF

# now generate the stubs
    c=0
    for s in $functions; do
        echo "JUMP_SLOT(\"$s\", $[c * 4]);" >>$outfile
        (( c++ ))
    done

    echo >>"$outfile"

    echo 'asm(".previous");' >>"$outfile"

    cat <<EOF >>"$outfile"

#ifdef __cplusplus
    }
#endif
EOF

}

arch_ok=false
case `arch` in
    i386 | i486 | i586 | i686 )
        arch_ok=true
esac

searchpath=( "/usr/lib" "/usr/local/lib" "/lib" `pwd` )
relaylist=( )

# process arguments
i=1
while (( i <= $# )); do
    a=${!i}
    
    if [[ ${a:0:2} == "-L" ]]; then
        searchpath[${#searchpath}]=${a:2}
        echo -n "$a " # copy to stdout

    elif [[ $a == "--partial-map" ]]; then
        using_partial_map=true
        (( i++ ))
        partial_map=${!i}
                
    elif [[ $a == "--relay" ]]; then
        (( i++ ))
        relaylist[${#relaylist}]=${!i}

    elif [[ ${a:0:2} == "-l" ]]; then
        lib=${a:2}
        
        # is this lib meant to be relayed?
        found=false
        for b in $relaylist; do
            if [[ "$b" == "$lib" ]]; then
                found=true
            fi
        done

        if $found && $arch_ok; then
        
            # yes, so let's find its absolute filename by checking in each search path directory
            spfound=false
            for d in ${searchpath[@]}; do
                if [ -e $d/lib$lib.so ]; then
                
                    absname=$( readlink -f $d/lib$lib.so )
                    if [[ $? != 0 ]] || [ ! -f "$absname" ]; then
                        error broken symlink $absname
                    fi
                    
                    stubfile=$( relay "$absname" )

                    # now we have to compile the stub
                    stubobj=$( echo $stubfile | sed 's/\.c$/\.o/' )
                    ${CC:-gcc} -fPIC -c -o $stubobj $stubfile 2>/dev/tty
                    echo -n "$stubobj "
                    spfound=true
                    break;
                fi
            done

            if ! $spfound; then
                error could not find $lib in search path
            fi
        elif $found && ! $arch_ok; then
            echo -n "-Dlib${lib}_is_present -D\"lib${lib}_symbol_is_present(sym)=1\" $a "
        else
            echo -n "$a "
        fi
        
    else
        # just copy whatever we don't recognise
        echo -n "$a "
    fi
    
    (( i++ ))
done
echo