diff --git a/Objectively.vs15/Sources/Windowly.c b/Objectively.vs15/Sources/Windowly.c index 0a5f50d..905337e 100644 --- a/Objectively.vs15/Sources/Windowly.c +++ b/Objectively.vs15/Sources/Windowly.c @@ -32,16 +32,6 @@ static const int64_t DELTA_EPOCH_IN_MICROSECS = 11644473600000000; -long __sync_val_compare_and_swap_(long volatile *Destination, long Exchange, long Comparand) { - return InterlockedCompareExchange(Destination, Exchange, Comparand); -} -long __sync_add_and_fetch_(long volatile *Append, long Value) { - return InterlockedAdd(Append, Value); -} -void *__sync_lock_test_and_set_(void *volatile *Target, void *Value) { - return InterlockedExchangePointer(Target, Value); -} - int gettimeofday(struct timeval *tv, struct timezone *tz) { if (tv) @@ -133,4 +123,4 @@ int asprintf(char **str, const char *fmt, ...) return size; } -#pragma endregion asprintf \ No newline at end of file +#pragma endregion asprintf diff --git a/Objectively.vs15/Sources/Windowly.h b/Objectively.vs15/Sources/Windowly.h index 4112050..27dbc8f 100644 --- a/Objectively.vs15/Sources/Windowly.h +++ b/Objectively.vs15/Sources/Windowly.h @@ -88,12 +88,3 @@ OBJECTIVELY_EXPORT int vasprintf(char **str, const char *fmt, va_list args); #endif #define iconv(c0, c1, c2, c3, c4) libiconv(c0, (const char **)c1, c2, c3, c4) - -// INTERLOCK STUFF -OBJECTIVELY_EXPORT long __sync_val_compare_and_swap_(long volatile *Destination, long Exchange, long Comparand); -OBJECTIVELY_EXPORT long __sync_add_and_fetch_(long volatile *Append, long Value); -OBJECTIVELY_EXPORT void *__sync_lock_test_and_set_(void *volatile *Target, void *Value); - -#define __sync_val_compare_and_swap(c0, c1, c2) __sync_val_compare_and_swap_((volatile long *) c0, c1, c2) -#define __sync_add_and_fetch(c0, c1) __sync_add_and_fetch_((volatile long *) c0, c1) -#define __sync_lock_test_and_set(c0, c1) __sync_lock_test_and_set_((void *volatile *) c0, (void *) c1) \ No newline at end of file diff --git a/Sources/Objectively/Class.c b/Sources/Objectively/Class.c index 22345cc..bb8a8f9 100644 --- a/Sources/Objectively/Class.c +++ b/Sources/Objectively/Class.c @@ -25,16 +25,13 @@ #include #include +#include #include #include #include #if defined(_WIN32) #include -#elif defined(__APPLE__) -#include -#else -#include #endif #if HAVE_UNISTD_H @@ -49,11 +46,29 @@ size_t _pageSize; static Class *_classes; /** - * @brief The registered images providing Classes. + * @brief Guards the structure of `_classes`. MUST NOT be held across `dlsym`, + * `dlopen`, or a Class initializer, each of which can reenter `_initialize`. */ -#define MAX_CLASS_IMAGES 8 -static ident _classImages[MAX_CLASS_IMAGES]; -static size_t _classImageCount; +static pthread_mutex_t _classesLock = PTHREAD_MUTEX_INITIALIZER; + +/** + * @brief A registered image: the handle the application holds, and the base + * address that the Classes it declares record in `Class::image`. + */ +typedef struct ClassImage ClassImage; +struct ClassImage { + ident handle; + ident image; + ClassImage *next; +}; + +/** + * @brief The registered images providing Classes, most recently added first. + * Published atomically rather than under `_classesLock`, which cannot be held + * across the `dlsym` this list exists for. A plain list rather than a + * MutableArray, because Class is beneath the collections. + */ +static ClassImage *_images; /** * @brief Called `atexit` to teardown Objectively. @@ -80,6 +95,16 @@ static void teardown(void) { c = next; } + + ClassImage *i = _images; + while (i) { + + ClassImage *next = i->next; + + free(i); + + i = next; + } } /** @@ -159,12 +184,14 @@ Class *_initialize(const ClassDef *def) { * a compound literal with automatic storage. */ clazz->image = imageForAddress((ident) def->name); - /* Link before publishing, and relaxed on the way in: the CAS is a read-modify- - * write, so it joins the release sequence of the push it displaces, and a - * reader acquiring the head synchronizes with every publisher behind it. The - * head is only copied here, never dereferenced, so there is nothing to acquire. */ - clazz->next = __atomic_load_n(&_classes, __ATOMIC_RELAXED); - while (!__atomic_compare_exchange_n(&_classes, &clazz->next, clazz, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) ; + /* Taken here rather than around the whole function: def.initialize above can + * reach other archetypes, and so this, before that Class is published. */ + pthread_mutex_lock(&_classesLock); + + clazz->next = _classes; + _classes = clazz; + + pthread_mutex_unlock(&_classesLock); return clazz; } @@ -207,105 +234,106 @@ ident _cast(const Class *clazz, const ident obj) { return (ident) obj; } -/** - * @return The base address of the image behind `handle`, or `NULL`. - * @remarks There is no one call for this. Windows hands out the module itself as - * the handle, which `GetModuleFileName` confirms rather than assumes; glibc - * answers from the link map; and macOS, which has neither, is left with matching - * the handle against the loaded images. - */ -static ident imageForHandle(ident handle) { +void addClassImage(ident handle, const ident address) { assert(handle); + assert(address); -#if defined(_WIN32) - char path[MAX_PATH]; - if (GetModuleFileNameA((HMODULE) handle, path, sizeof(path))) { - return handle; + const ident image = imageForAddress(address); + if (image == NULL) { + fprintf(stderr, "%s: no image contains %p\n", __func__, address); + abort(); } -#elif defined(__APPLE__) - for (uint32_t i = 0; i < _dyld_image_count(); i++) { - - /* RTLD_LOCAL is not the default on macOS, and dlopen of an image already - * loaded promotes it to the global namespace, which would put every image in - * the process there - undoing the isolation the caller loaded it for. */ - ident image = dlopen(_dyld_get_image_name(i), RTLD_LAZY | RTLD_NOLOAD | RTLD_LOCAL); - if (image == NULL) { - continue; - } - dlclose(image); + ClassImage *classImage = calloc(1, sizeof(ClassImage)); + assert(classImage); - if (image == handle) { - return (ident) _dyld_get_image_header(i); - } - } -#else - struct link_map *map; - if (dlinfo(handle, RTLD_DI_LINKMAP, &map) == 0 && map) { - return (ident) map->l_addr; - } -#endif + classImage->handle = handle; + classImage->image = image; - return NULL; + /* Published the same way a Class is, and for the same reason: classForName + * walks this list on any thread. */ + classImage->next = __atomic_load_n(&_images, __ATOMIC_RELAXED); + while (!__atomic_compare_exchange_n(&_images, &classImage->next, classImage, 1, + __ATOMIC_RELEASE, __ATOMIC_RELAXED)) ; } void removeClassImage(ident handle) { assert(handle); - for (size_t i = 0; i < _classImageCount; i++) { - if (_classImages[i] == handle) { - memmove(_classImages + i, _classImages + i + 1, (_classImageCount - i - 1) * sizeof(ident)); - _classImages[--_classImageCount] = NULL; + /* Held from the search to the last unlink, so that retiring an image and + * dropping its Classes is one operation, and a second call for the same handle + * finds it already gone. Nothing here reaches the loader. */ + pthread_mutex_lock(&_classesLock); + + ident image = NULL; + + /* Retired in place rather than unlinked, so that a concurrent classForName + * parked on this node still has a next to follow, and never reads a node that + * has been freed. Retiring is a single store of the handle it matches on, so + * that walk sees this image or does not, and never half of it. Retired nodes + * are freed at teardown; reusing one would put a newly registered image where + * the retired one sat, and lookup order is newest first. */ + for (ClassImage *i = __atomic_load_n(&_images, __ATOMIC_ACQUIRE); i; i = i->next) { + if (__atomic_load_n(&i->handle, __ATOMIC_ACQUIRE) == handle) { + image = i->image; + __atomic_store_n(&i->handle, NULL, __ATOMIC_RELEASE); break; } } - const ident image = imageForHandle(handle); if (image == NULL) { - return; + fprintf(stderr, "%s: %p was never registered\n", __func__, handle); + abort(); } - Class **link = &_classes; - while (*link) { - Class *clazz = *link; + Class **classes = &_classes; + while (*classes) { + Class *clazz = *classes; if (clazz->image == image) { - *link = clazz->next; + *classes = clazz->next; clazz->next = NULL; } else { - link = &clazz->next; + classes = &clazz->next; } } -} -void addClassImage(ident handle) { - - assert(handle); - assert(_classImageCount < MAX_CLASS_IMAGES); - - _classImages[_classImageCount++] = handle; + pthread_mutex_unlock(&_classesLock); } Class *classForName(const char *name) { if (name) { - Class *c = __atomic_load_n(&_classes, __ATOMIC_ACQUIRE); + pthread_mutex_lock(&_classesLock); + + Class *c = _classes; while (c) { if (strcmp(name, c->def.name) == 0) { - return c; + break; } c = c->next; } + pthread_mutex_unlock(&_classesLock); + + if (c) { + return c; + } + char *s; if (asprintf(&s, "_%s", name) > 0) { Class *clazz = NULL; Class *(*archetype)(void) = NULL; - for (size_t i = _classImageCount; i > 0 && archetype == NULL; i--) { - archetype = dlsym(_classImages[i - 1], s); + for (ClassImage *i = __atomic_load_n(&_images, __ATOMIC_ACQUIRE); + i && archetype == NULL; i = i->next) { + + ident handle = __atomic_load_n(&i->handle, __ATOMIC_ACQUIRE); + if (handle) { + archetype = dlsym(handle, s); + } } if (archetype == NULL) { diff --git a/Sources/Objectively/Class.h b/Sources/Objectively/Class.h index 0365fce..c900113 100644 --- a/Sources/Objectively/Class.h +++ b/Sources/Objectively/Class.h @@ -138,6 +138,8 @@ OBJECTIVELY_EXPORT ident _cast(const Class *clazz, const ident obj); /** * @brief Registers an image that provides Classes, e.g. a plugin. * @param handle A handle from `dlopen`. + * @param address Any address within that image, such as the entry point the + * application resolved from `handle`. * @details `classForName` resolves a name it has not yet initialized through the * process-wide namespace, which holds only images loaded `RTLD_GLOBAL`, and which * Windows does not have at all. An application that loads Classes from a plugin @@ -147,8 +149,12 @@ OBJECTIVELY_EXPORT ident _cast(const Class *clazz, const ident obj); * loaded plugin answers ahead of the one it replaced. * @remarks Resolution is by Objectively's own convention, the Class name * prefixed with an underscore, so nothing about the image has to be declared. + * @remarks `address` is what identifies the image. Classes record the base + * address of the image that declared them, and the platforms report that for an + * address, not for a handle, so this takes one that the caller can vouch for. + * Aborts if no loaded image contains it. */ -OBJECTIVELY_EXPORT void addClassImage(ident handle); +OBJECTIVELY_EXPORT void addClassImage(ident handle, const ident address); /** * @brief Unregisters an image, and every Class it declared. @@ -168,6 +174,9 @@ OBJECTIVELY_EXPORT void addClassImage(ident handle); * and its interface, which are not reclaimed. * @remarks MUST be called while the handle is still open, and only once nothing * instantiated from that image survives. + * @remarks Aborts if `handle` was not registered, rather than leaving the + * Classes it declared behind, which is the failure this exists to prevent. Two + * calls for the same handle abort on the second, whichever order they arrive in. */ OBJECTIVELY_EXPORT void removeClassImage(ident handle); diff --git a/Tests/Objectively/Locale b/Tests/Objectively/Locale deleted file mode 100755 index b3638ff..0000000 --- a/Tests/Objectively/Locale +++ /dev/null @@ -1,210 +0,0 @@ -#! /bin/sh - -# Locale - temporary wrapper script for .libs/Locale -# Generated by libtool (GNU libtool) 2.4.6 -# -# The Locale program cannot be directly executed until all the libtool -# libraries that it depends on are installed. -# -# This wrapper script should never be moved out of the build directory. -# If it is, it will not operate correctly. - -# Sed substitution that helps us do robust quoting. It backslashifies -# metacharacters that are still active within double-quoted strings. -sed_quote_subst='s|\([`"$\\]\)|\\\1|g' - -# Be Bourne compatible -if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then - emulate sh - NULLCMD=: - # Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which - # is contrary to our usage. Disable this feature. - alias -g '${1+"$@"}'='"$@"' - setopt NO_GLOB_SUBST -else - case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac -fi -BIN_SH=xpg4; export BIN_SH # for Tru64 -DUALCASE=1; export DUALCASE # for MKS sh - -# The HP-UX ksh and POSIX shell print the target directory to stdout -# if CDPATH is set. -(unset CDPATH) >/dev/null 2>&1 && unset CDPATH - -relink_command="" - -# This environment variable determines our operation mode. -if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then - # install mode needs the following variables: - generated_by_libtool_version='2.4.6' - notinst_deplibs=' ../../Sources/Objectively/libObjectively.la' -else - # When we are sourced in execute mode, $file and $ECHO are already set. - if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then - file="$0" - -# A function that is used when there is no print builtin or printf. -func_fallback_echo () -{ - eval 'cat <<_LTECHO_EOF -$1 -_LTECHO_EOF' -} - ECHO="printf %s\\n" - fi - -# Very basic option parsing. These options are (a) specific to -# the libtool wrapper, (b) are identical between the wrapper -# /script/ and the wrapper /executable/ that is used only on -# windows platforms, and (c) all begin with the string --lt- -# (application programs are unlikely to have options that match -# this pattern). -# -# There are only two supported options: --lt-debug and -# --lt-dump-script. There is, deliberately, no --lt-help. -# -# The first argument to this parsing function should be the -# script's ../../libtool value, followed by no. -lt_option_debug= -func_parse_lt_options () -{ - lt_script_arg0=$0 - shift - for lt_opt - do - case "$lt_opt" in - --lt-debug) lt_option_debug=1 ;; - --lt-dump-script) - lt_dump_D=`$ECHO "X$lt_script_arg0" | /opt/local/bin/gsed -e 's/^X//' -e 's%/[^/]*$%%'` - test "X$lt_dump_D" = "X$lt_script_arg0" && lt_dump_D=. - lt_dump_F=`$ECHO "X$lt_script_arg0" | /opt/local/bin/gsed -e 's/^X//' -e 's%^.*/%%'` - cat "$lt_dump_D/$lt_dump_F" - exit 0 - ;; - --lt-*) - $ECHO "Unrecognized --lt- option: '$lt_opt'" 1>&2 - exit 1 - ;; - esac - done - - # Print the debug banner immediately: - if test -n "$lt_option_debug"; then - echo "Locale:Locale:$LINENO: libtool wrapper (GNU libtool) 2.4.6" 1>&2 - fi -} - -# Used when --lt-debug. Prints its arguments to stdout -# (redirection is the responsibility of the caller) -func_lt_dump_args () -{ - lt_dump_args_N=1; - for lt_arg - do - $ECHO "Locale:Locale:$LINENO: newargv[$lt_dump_args_N]: $lt_arg" - lt_dump_args_N=`expr $lt_dump_args_N + 1` - done -} - -# Core function for launching the target application -func_exec_program_core () -{ - - if test -n "$lt_option_debug"; then - $ECHO "Locale:Locale:$LINENO: newargv[0]: $progdir/$program" 1>&2 - func_lt_dump_args ${1+"$@"} 1>&2 - fi - exec "$progdir/$program" ${1+"$@"} - - $ECHO "$0: cannot exec $program $*" 1>&2 - exit 1 -} - -# A function to encapsulate launching the target application -# Strips options in the --lt-* namespace from $@ and -# launches target application with the remaining arguments. -func_exec_program () -{ - case " $* " in - *\ --lt-*) - for lt_wr_arg - do - case $lt_wr_arg in - --lt-*) ;; - *) set x "$@" "$lt_wr_arg"; shift;; - esac - shift - done ;; - esac - func_exec_program_core ${1+"$@"} -} - - # Parse options - func_parse_lt_options "$0" ${1+"$@"} - - # Find the directory that this script lives in. - thisdir=`$ECHO "$file" | /opt/local/bin/gsed 's%/[^/]*$%%'` - test "x$thisdir" = "x$file" && thisdir=. - - # Follow symbolic links until we get to the real thisdir. - file=`ls -ld "$file" | /opt/local/bin/gsed -n 's/.*-> //p'` - while test -n "$file"; do - destdir=`$ECHO "$file" | /opt/local/bin/gsed 's%/[^/]*$%%'` - - # If there was a directory component, then change thisdir. - if test "x$destdir" != "x$file"; then - case "$destdir" in - [\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;; - *) thisdir="$thisdir/$destdir" ;; - esac - fi - - file=`$ECHO "$file" | /opt/local/bin/gsed 's%^.*/%%'` - file=`ls -ld "$thisdir/$file" | /opt/local/bin/gsed -n 's/.*-> //p'` - done - - # Usually 'no', except on cygwin/mingw when embedded into - # the cwrapper. - WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no - if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then - # special case for '.' - if test "$thisdir" = "."; then - thisdir=`pwd` - fi - # remove .libs from thisdir - case "$thisdir" in - *[\\/].libs ) thisdir=`$ECHO "$thisdir" | /opt/local/bin/gsed 's%[\\/][^\\/]*$%%'` ;; - .libs ) thisdir=. ;; - esac - fi - - # Try to get the absolute directory name. - absdir=`cd "$thisdir" && pwd` - test -n "$absdir" && thisdir="$absdir" - - program='Locale' - progdir="$thisdir/.libs" - - - if test -f "$progdir/$program"; then - # Add our own library path to DYLD_LIBRARY_PATH - DYLD_LIBRARY_PATH="/Users/jdolan/Coding/Objectively/Sources/Objectively/.libs:$DYLD_LIBRARY_PATH" - - # Some systems cannot cope with colon-terminated DYLD_LIBRARY_PATH - # The second colon is a workaround for a bug in BeOS R4 sed - DYLD_LIBRARY_PATH=`$ECHO "$DYLD_LIBRARY_PATH" | /opt/local/bin/gsed 's/::*$//'` - - export DYLD_LIBRARY_PATH - - if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then - # Run the actual program with our arguments. - func_exec_program ${1+"$@"} - fi - else - # The program doesn't exist. - $ECHO "$0: error: '$progdir/$program' does not exist" 1>&2 - $ECHO "This script is just a wrapper for $program." 1>&2 - $ECHO "See the libtool documentation for more information." 1>&2 - exit 1 - fi -fi