diff --git a/include/git2/sys/custom_tls.h b/include/git2/sys/custom_tls.h new file mode 100644 index 00000000000..73832206120 --- /dev/null +++ b/include/git2/sys/custom_tls.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_sys_custom_tls_h__ +#define INCLUDE_sys_custom_tls_h__ + +#include "git2/common.h" + +GIT_BEGIN_DECL + +/** + * Used to retrieve a pointer from a user of the library to pass to a newly + * created internal libgit2 thread. This should allow users of the library to + * establish a context that spans an internally threaded operation. This can + * useful for libraries that leverage callbacks used in an internally threaded + * routine. + */ +typedef void *GIT_CALLBACK(git_retrieve_tls_for_internal_thread_cb)(void); + +/** + * This callback will be called when a thread is exiting so that a user + * of the library can clean up their thread local storage. + */ +typedef void GIT_CALLBACK(git_set_tls_on_internal_thread_cb)(void *payload); + +/** + * This callback will be called when a thread is exiting so that a user + * of the library can clean up their thread local storage. + */ +typedef void GIT_CALLBACK(git_teardown_tls_on_internal_thread_cb)(void); + +/** + * Sets the callbacks for custom thread local storage used by internally + * created libgit2 threads. This allows users of the library an opportunity + * to set thread local storage for internal threads based on the creating + * thread. + * + * @param retrieve_storage_for_internal_thread Used to retrieve a pointer on + * a thread before spawning child + * threads. This pointer will be + * passed to set_storage_on_thread + * in the newly spawned threads. + * @param set_storage_on_thread When a thread is spawned internally in libgit2, + * whatever pointer was retrieved in the calling + * thread by retrieve_storage_for_internal_thread + * will be passed to this callback in the newly + * spawned thread. + * @param teardown_storage_on_thread Before an internally spawned thread exits, + * this method will be called allowing a user + * of the library an opportunity to clean up + * any thread local storage they set up on + * the internal thread. + * @return 0 on success, or an error code. (use git_error_last for information + * about the error) + */ +GIT_EXTERN(int) git_custom_tls_set_callbacks( + git_retrieve_tls_for_internal_thread_cb retrieve_storage_for_internal_thread, + git_set_tls_on_internal_thread_cb set_storage_on_thread, + git_teardown_tls_on_internal_thread_cb teardown_storage_on_thread); + +GIT_END_DECL + +#endif diff --git a/src/libgit2/libgit2.c b/src/libgit2/libgit2.c index ce287147a70..890d3e35220 100644 --- a/src/libgit2/libgit2.c +++ b/src/libgit2/libgit2.c @@ -12,6 +12,7 @@ #include "buf.h" #include "cache.h" #include "common.h" +#include "custom_tls.h" #include "filter.h" #include "grafts.h" #include "hash.h" @@ -76,6 +77,7 @@ int git_libgit2_init(void) git_threadstate_global_init, git_threads_global_init, git_rand_global_init, + git_custom_tls__global_init, git_hash_global_init, git_sysdir_global_init, git_filter_global_init, diff --git a/src/util/custom_tls.c b/src/util/custom_tls.c new file mode 100644 index 00000000000..6a1fb8a77d2 --- /dev/null +++ b/src/util/custom_tls.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "runtime.h" + +#ifdef GIT_THREADS + +#ifdef GIT_WIN32 +# include "win32/thread.h" +#else +# include "unix/pthread.h" +#endif + +struct git_custom_tls_callbacks { + git_retrieve_tls_for_internal_thread_cb retrieve_storage_for_internal_thread; + + git_set_tls_on_internal_thread_cb set_storage_on_thread; + + git_teardown_tls_on_internal_thread_cb teardown_storage_on_thread; + + git_rwlock lock; +}; + +struct git_custom_tls_callbacks git__custom_tls = { 0, 0, 0 }; + +static void git_custom_tls_global_shutdown(void) +{ + if (git_rwlock_wrlock(&git__custom_tls.lock) < 0) + return; + + git__custom_tls.retrieve_storage_for_internal_thread = 0; + git__custom_tls.set_storage_on_thread = 0; + git__custom_tls.teardown_storage_on_thread = 0; + + git_rwlock_wrunlock(&git__custom_tls.lock); + git_rwlock_free(&git__custom_tls.lock); +} + +int git_custom_tls__global_init(void) +{ + if (git_rwlock_init(&git__custom_tls.lock) < 0) + return -1; + + return git_runtime_shutdown_register(git_custom_tls_global_shutdown); +} + +int git_custom_tls_set_callbacks( + git_retrieve_tls_for_internal_thread_cb retrieve_storage_for_internal_thread, + git_set_tls_on_internal_thread_cb set_storage_on_thread, + git_teardown_tls_on_internal_thread_cb teardown_storage_on_thread) +{ + /* We want to ensure that all callbacks are set or not set in totality. + * It does not make sense to have a subset of callbacks set. + */ + assert((retrieve_storage_for_internal_thread && set_storage_on_thread && + teardown_storage_on_thread) || !(retrieve_storage_for_internal_thread && + set_storage_on_thread && teardown_storage_on_thread)); + + if (git_rwlock_wrlock(&git__custom_tls.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock custom thread local storage"); + return -1; + } + + git__custom_tls.retrieve_storage_for_internal_thread = + retrieve_storage_for_internal_thread; + git__custom_tls.set_storage_on_thread = + set_storage_on_thread; + git__custom_tls.teardown_storage_on_thread = + teardown_storage_on_thread; + + git_rwlock_wrunlock(&git__custom_tls.lock); + return 0; +} + +int git_custom_tls__init(git_custom_tls *tls) +{ + if (git_rwlock_rdlock(&git__custom_tls.lock) < 0) { + git_error_set(GIT_ERROR_OS, "failed to lock custom thread local storage"); + return -1; + } + + /* We try to ensure that all 3 callbacks must be set or not set. + * It would not make sense to have a subset of the callbacks set. + */ + if (!git__custom_tls.retrieve_storage_for_internal_thread) { + tls->set_storage_on_thread = NULL; + tls->teardown_storage_on_thread = NULL; + tls->payload = NULL; + } else { + /* We set these on a struct so that if for whatever reason the opts are changed + * at least the opts will remain consistent for any given thread already in + * motion. + */ + tls->set_storage_on_thread = git__custom_tls.set_storage_on_thread; + tls->teardown_storage_on_thread = git__custom_tls.teardown_storage_on_thread; + tls->payload = git__custom_tls.retrieve_storage_for_internal_thread(); + } + + git_rwlock_rdunlock(&git__custom_tls.lock); + return 0; +} + +#else + +int git_custom_tls__global_init(void) +{ + return 0; +} + +int git_custom_tls_set_callbacks( + git_retrieve_tls_for_internal_thread_cb retrieve_storage_for_internal_thread, + git_set_tls_on_internal_thread_cb set_storage_on_thread, + git_teardown_tls_on_internal_thread_cb teardown_storage_on_thread) +{ + return 0; +} + +#endif diff --git a/src/util/custom_tls.h b/src/util/custom_tls.h new file mode 100644 index 00000000000..c9af23564a9 --- /dev/null +++ b/src/util/custom_tls.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_custom_tls_h__ +#define INCLUDE_custom_tls_h__ + +#include "git2/sys/custom_tls.h" + +int git_custom_tls__global_init(void); + +#ifdef GIT_THREADS + +typedef struct { + git_set_tls_on_internal_thread_cb set_storage_on_thread; + + git_teardown_tls_on_internal_thread_cb teardown_storage_on_thread; + + /** + * payload should be set on the thread that is spawning the child thread. + * This payload will be passed to set_storage_on_thread + */ + void *payload; +} git_custom_tls; + +int git_custom_tls__init(git_custom_tls *tls); + +#endif + +#endif diff --git a/src/util/unix/pthread.c b/src/util/unix/pthread.c new file mode 100644 index 00000000000..796061b6cdd --- /dev/null +++ b/src/util/unix/pthread.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "pthread.h" +#include "thread.h" +#include "runtime.h" + +git_tlsdata_key thread_handle; + +static void git_threads_global_shutdown(void) { + git_tlsdata_dispose(thread_handle); +} + +int git_threads_global_init(void) { + int error = git_tlsdata_init(&thread_handle, NULL); + if (error != 0) { + return error; + } + + return git_runtime_shutdown_register(git_threads_global_shutdown); +} + +static void *git_unix__threadproc(void *arg) +{ + void *result; + int error; + git_thread *thread = arg; + + error = git_tlsdata_set(thread_handle, thread); + if (error != 0) { + return NULL; + } + + if (thread->tls.set_storage_on_thread) { + thread->tls.set_storage_on_thread(thread->tls.payload); + } + + result = thread->proc(thread->param); + + if (thread->tls.teardown_storage_on_thread) { + thread->tls.teardown_storage_on_thread(); + } + + return result; +} + +int git_thread_create( + git_thread *thread, + void *(*start_routine)(void*), + void *arg) +{ + + thread->proc = start_routine; + thread->param = arg; + if (git_custom_tls__init(&thread->tls) < 0) + return -1; + + return pthread_create(&thread->thread, NULL, git_unix__threadproc, thread); +} + +void git_thread_exit(void *value) +{ + git_thread *thread = git_tlsdata_get(thread_handle); + + if (thread && thread->tls.teardown_storage_on_thread) + thread->tls.teardown_storage_on_thread(); + + return pthread_exit(value); +} diff --git a/src/util/unix/pthread.h b/src/util/unix/pthread.h index 55f4ae227ee..2a1e8809fdf 100644 --- a/src/util/unix/pthread.h +++ b/src/util/unix/pthread.h @@ -8,18 +8,26 @@ #ifndef INCLUDE_unix_pthread_h__ #define INCLUDE_unix_pthread_h__ +#include "git2_util.h" +#include "custom_tls.h" + typedef struct { pthread_t thread; + void *(*proc)(void *); + void *param; + git_custom_tls tls; } git_thread; -GIT_INLINE(int) git_threads_global_init(void) { return 0; } +int git_threads_global_init(void); -#define git_thread_create(git_thread_ptr, start_routine, arg) \ - pthread_create(&(git_thread_ptr)->thread, NULL, start_routine, arg) +int git_thread_create( + git_thread *thread, + void *(*start_routine)(void*), + void *arg); #define git_thread_join(git_thread_ptr, status) \ pthread_join((git_thread_ptr)->thread, status) #define git_thread_currentid() ((size_t)(pthread_self())) -#define git_thread_exit(retval) pthread_exit(retval) +void git_thread_exit(void *value); /* Git Mutex */ #define git_mutex pthread_mutex_t diff --git a/src/util/win32/thread.c b/src/util/win32/thread.c index f5cacd320d8..b3b5b0cb3c8 100644 --- a/src/util/win32/thread.c +++ b/src/util/win32/thread.c @@ -31,8 +31,16 @@ static DWORD WINAPI git_win32__threadproc(LPVOID lpParameter) /* Set the current thread for `git_thread_exit` */ FlsSetValue(fls_index, thread); + if (thread->tls.set_storage_on_thread) { + thread->tls.set_storage_on_thread(thread->tls.payload); + } + thread->result = thread->proc(thread->param); + if (thread->tls.teardown_storage_on_thread) { + thread->tls.teardown_storage_on_thread(); + } + return CLEAN_THREAD_EXIT; } @@ -72,6 +80,9 @@ int git_thread_create( thread->result = NULL; thread->param = arg; thread->proc = start_routine; + if (git_custom_tls__init(&thread->tls) < 0) + return -1; + thread->thread = CreateThread( NULL, 0, git_win32__threadproc, thread, 0, NULL); @@ -107,8 +118,11 @@ void git_thread_exit(void *value) { git_thread *thread = FlsGetValue(fls_index); - if (thread) + if (thread) { + if (thread->tls.teardown_storage_on_thread) + thread->tls.teardown_storage_on_thread(); thread->result = value; + } ExitThread(CLEAN_THREAD_EXIT); } diff --git a/src/util/win32/thread.h b/src/util/win32/thread.h index 184762e2aa9..6ea34ace780 100644 --- a/src/util/win32/thread.h +++ b/src/util/win32/thread.h @@ -9,6 +9,7 @@ #define INCLUDE_win32_thread_h__ #include "git2_util.h" +#include "custom_tls.h" #if defined (_MSC_VER) # define GIT_RESTRICT __restrict @@ -21,6 +22,7 @@ typedef struct { void *(*proc)(void *); void *param; void *result; + git_custom_tls tls; } git_thread; typedef CRITICAL_SECTION git_mutex; diff --git a/tests/threads/custom_tls.c b/tests/threads/custom_tls.c new file mode 100644 index 00000000000..cd4a059d98d --- /dev/null +++ b/tests/threads/custom_tls.c @@ -0,0 +1,155 @@ +#include "clar_libgit2.h" + +#include "thread_helpers.h" +#include "alloc.h" +#include "common.h" +#include "git2/sys/custom_tls.h" + +static int *test[2] = { NULL, NULL }; +static int num_threads_spawned = 0; + +#if defined(GIT_THREADS) && defined(GIT_WIN32) +static DWORD _fls_index; + +int init_thread_local_storage(void) +{ + if ((_fls_index = FlsAlloc(NULL)) == FLS_OUT_OF_INDEXES) + return -1; + + return 0; +} + +void cleanup_thread_local_storage(void) +{ + FlsFree(_fls_index); +} + +void *init_local_storage(void) { + test[num_threads_spawned] = git__calloc(1, sizeof(int)); + return test[num_threads_spawned++]; +} + +void init_tls(void *payload) { + int *i = payload; + (*i)++; + FlsSetValue(_fls_index, i); +} + +void teardown_tls(void) { + int *i = FlsGetValue(_fls_index); + (*i)++; +} + +#elif defined(GIT_THREADS) && defined(_POSIX_THREADS) +static pthread_key_t _tls_key; + +int init_thread_local_storage(void) +{ + return pthread_key_create(&_tls_key, NULL); +} + +void cleanup_thread_local_storage(void) +{ + pthread_key_delete(_tls_key); +} + +void *init_local_storage(void) { + test[num_threads_spawned] = git__calloc(1, sizeof(int)); + return test[num_threads_spawned++]; +} + +void init_tls(void *payload) { + int *i = payload; + (*i)++; + pthread_setspecific(_tls_key, i); +} + +void teardown_tls(void) { + int *i = pthread_getspecific(_tls_key); + (*i)++; +} + +#endif + +void test_threads_custom_tls__initialize(void) +{ +#ifdef GIT_THREADS + cl_git_pass(init_thread_local_storage()); + cl_git_pass(git_custom_tls_set_callbacks(init_local_storage, init_tls, teardown_tls)); + test[0] = NULL; + test[1] = NULL; + num_threads_spawned = 0; +#endif +} + +void test_threads_custom_tls__cleanup(void) +{ +#ifdef GIT_THREADS + cleanup_thread_local_storage(); + git_custom_tls_set_callbacks(NULL, NULL, NULL); + + git__free(test[0]); + test[0] = NULL; + + git__free(test[1]); + test[1] = NULL; +#endif +} + +#ifdef GIT_THREADS +static void *return_normally(void *param) +{ + return param; +} +#endif + +void test_threads_custom_tls__multiple_clean_exit(void) +{ +#ifndef GIT_THREADS + clar__skip(); +#else + git_thread thread1, thread2; + void *result; + + cl_git_pass(git_thread_create(&thread1, return_normally, (void *)424242)); + cl_git_pass(git_thread_create(&thread2, return_normally, (void *)232323)); + + cl_git_pass(git_thread_join(&thread1, &result)); + cl_assert_equal_sz(424242, (size_t)result); + cl_git_pass(git_thread_join(&thread2, &result)); + cl_assert_equal_sz(232323, (size_t)result); + + cl_assert_equal_i(2, *(test[0])); + cl_assert_equal_i(2, *(test[1])); +#endif +} + +#ifdef GIT_THREADS +static void *return_early(void *param) +{ + git_thread_exit(param); + assert(false); + return param; +} +#endif + +void test_threads_custom_tls__multiple_threads_use_exit(void) +{ +#ifndef GIT_THREADS + clar__skip(); +#else + git_thread thread1, thread2; + void *result; + + cl_git_pass(git_thread_create(&thread1, return_early, (void *)424242)); + cl_git_pass(git_thread_create(&thread2, return_early, (void *)232323)); + + cl_git_pass(git_thread_join(&thread1, &result)); + cl_assert_equal_sz(424242, (size_t)result); + cl_git_pass(git_thread_join(&thread2, &result)); + cl_assert_equal_sz(232323, (size_t)result); + + cl_assert_equal_i(2, *(test[0])); + cl_assert_equal_i(2, *(test[1])); +#endif +}