diff --git a/c/CMakeLists.txt b/c/CMakeLists.txt index 398d321..3e679b1 100644 --- a/c/CMakeLists.txt +++ b/c/CMakeLists.txt @@ -103,6 +103,12 @@ target_link_libraries(json_structure PUBLIC cjson ) +# Link pthread on Unix systems for thread synchronization +if(NOT WIN32) + find_package(Threads REQUIRED) + target_link_libraries(json_structure PRIVATE Threads::Threads) +endif() + # Optional PCRE2 for regex if(JS_ENABLE_REGEX) find_package(PkgConfig) diff --git a/c/README.md b/c/README.md index c75666b..d9ff729 100644 --- a/c/README.md +++ b/c/README.md @@ -166,6 +166,67 @@ int main(void) { } ``` +## Thread Safety + +The JSON Structure C SDK is designed to be thread-safe for concurrent validation operations when used correctly: + +### Thread-Safe Operations + +- **Concurrent validation calls**: Multiple threads can safely call `js_validate_schema()`, `js_validate_instance()`, and related validation functions simultaneously. +- **Memory allocation**: All memory allocation operations (`js_malloc()`, `js_realloc()`, `js_free()`) are protected by internal synchronization primitives. +- **Regex compilation cache**: The internal regex cache uses mutexes to ensure thread-safe access. + +### Usage Requirements + +For thread-safe operation, follow these guidelines: + +1. **Initialize once before threading**: + ```c + int main(void) { + // Call js_init() once at program startup, before creating threads + js_init(); + + // Now safe to create threads that perform validation + // ... + + return 0; + } + ``` + +2. **Do not change allocator during validation**: + ```c + // Set custom allocator BEFORE any validation operations + js_init_with_allocator(my_allocator); + + // Do NOT call js_set_allocator() while validation is in progress + ``` + +3. **Clean up after all threads complete**: + ```c + // Ensure all validation threads have finished + // join_all_threads(); + + // Then call cleanup once + js_cleanup(); + ``` + +### Thread-Safety Guarantees + +- ✅ **Safe**: Concurrent calls to validation functions from multiple threads +- ✅ **Safe**: Reading the allocator configuration during validation +- ⚠️ **Unsafe**: Calling `js_set_allocator()` or `js_init_with_allocator()` while validation is in progress +- ⚠️ **Unsafe**: Calling `js_cleanup()` while validation is in progress + +### Testing with ThreadSanitizer + +To verify thread safety in your application, compile with ThreadSanitizer: + +```bash +cmake .. -DCMAKE_C_FLAGS="-fsanitize=thread -g" -DCMAKE_CXX_FLAGS="-fsanitize=thread -g" +cmake --build . +ctest +``` + ## API Reference ### Core Types diff --git a/c/include/json_structure/json_structure.h b/c/include/json_structure/json_structure.h index 6913e4f..f4dd9a3 100644 --- a/c/include/json_structure/json_structure.h +++ b/c/include/json_structure/json_structure.h @@ -37,7 +37,12 @@ extern "C" { * @brief Initialize the JSON Structure library * * Call this once at program startup. This function is optional if you - * don't need custom memory allocation. + * don't need custom memory allocation, but it's recommended for explicit + * initialization of internal resources. + * + * @note Thread-safety: This function should be called once before any + * validation operations. It initializes internal synchronization + * primitives used for thread-safe operation. */ JS_API void js_init(void); @@ -46,13 +51,31 @@ JS_API void js_init(void); * @param alloc Custom allocator functions * * Call this once at program startup if you need custom memory allocation. + * This function initializes the library and sets the custom allocator. + * + * @note Thread-safety: This function should be called once before any + * validation operations. Do not call this concurrently from multiple + * threads or while validation is in progress. */ JS_API void js_init_with_allocator(js_allocator_t alloc); /** * @brief Clean up the JSON Structure library * - * Call this at program shutdown to release any internal resources. + * Call this at program shutdown to release any internal resources, + * including the regex compilation cache and synchronization primitives. + * + * After calling js_cleanup(), you can call js_init() or + * js_init_with_allocator() again to reinitialize the library if needed. + * + * @note Thread-safety: This function must only be called when no + * validation operations are in progress. Calling this while + * validations are running leads to undefined behavior. + * + * @note The internal mutex is initialized once using pthread_once (Unix) + * or InitOnceExecuteOnce (Windows) for thread safety. While the + * library can be reinitialized after cleanup, the one-time + * initialization mechanism persists for the process lifetime. */ JS_API void js_cleanup(void); diff --git a/c/include/json_structure/types.h b/c/include/json_structure/types.h index 273626c..4bfc200 100644 --- a/c/include/json_structure/types.h +++ b/c/include/json_structure/types.h @@ -252,6 +252,10 @@ typedef struct js_allocator { * @param alloc Custom allocator with malloc, realloc, and free functions * * @note Pass NULL functions to reset to default allocator + * @note Thread-safety: This function is thread-safe but should only be called + * during initialization (from js_init_with_allocator() or before any + * validation calls). Changing the allocator while validation is in + * progress may lead to undefined behavior. */ JS_API void js_set_allocator(js_allocator_t alloc); diff --git a/c/src/json_source_locator.c b/c/src/json_source_locator.c index 973f065..eb0faad 100644 --- a/c/src/json_source_locator.c +++ b/c/src/json_source_locator.c @@ -387,17 +387,32 @@ js_location_t js_get_path_location(const char* source, const char* path) { * ============================================================================ */ void js_init(void) { - /* No initialization needed for now */ + /* Initialize allocator mutex */ + extern void js_init_allocator_mutex(void); + js_init_allocator_mutex(); } void js_init_with_allocator(js_allocator_t alloc) { + /* Initialize allocator mutex first */ + extern void js_init_allocator_mutex(void); + js_init_allocator_mutex(); + + /* Then set the custom allocator */ js_set_allocator(alloc); } void js_cleanup(void) { + /* Clear regex cache to free all compiled patterns */ + extern void js_regex_cache_clear(void); + js_regex_cache_clear(); + /* Reset to default allocator */ js_allocator_t default_alloc = {NULL, NULL, NULL, NULL}; js_set_allocator(default_alloc); + + /* Destroy allocator mutex */ + extern void js_destroy_allocator_mutex(void); + js_destroy_allocator_mutex(); } /* ============================================================================ diff --git a/c/src/types.c b/c/src/types.c index c3365b5..b1c1159 100644 --- a/c/src/types.c +++ b/c/src/types.c @@ -11,6 +11,36 @@ #include #include +/* Thread synchronization for allocator + * + * On Windows, we use SRWLOCK (Slim Reader/Writer Lock) which has these benefits: + * - No initialization required (zero-initialized by default) + * - No cleanup/destruction needed + * - Very lightweight and fast + * - Available on Windows Vista and later + * + * On Unix, we use pthread_mutex with pthread_once for thread-safe initialization. + */ +#if defined(_WIN32) +#include +typedef SRWLOCK js_mutex_t; +#define JS_MUTEX_STATIC_INIT SRWLOCK_INIT +#define JS_MUTEX_LOCK(m) AcquireSRWLockExclusive(m) +#define JS_MUTEX_UNLOCK(m) ReleaseSRWLockExclusive(m) +/* SRWLOCK needs no init or destroy */ +#define JS_MUTEX_INIT(m) ((void)0) +#define JS_MUTEX_DESTROY(m) ((void)0) +#else +#include +typedef pthread_mutex_t js_mutex_t; +typedef pthread_once_t js_once_t; +#define JS_ONCE_INIT PTHREAD_ONCE_INIT +#define JS_MUTEX_INIT(m) pthread_mutex_init(m, NULL) +#define JS_MUTEX_LOCK(m) pthread_mutex_lock(m) +#define JS_MUTEX_UNLOCK(m) pthread_mutex_unlock(m) +#define JS_MUTEX_DESTROY(m) pthread_mutex_destroy(m) +#endif + /* ============================================================================ * Default Allocator * ============================================================================ */ @@ -35,11 +65,60 @@ static js_allocator_t g_allocator = { NULL }; +/* Mutex to protect allocator access + * On Windows, SRWLOCK is zero-initialized by default which equals SRWLOCK_INIT, + * so no explicit initialization needed. + * On Unix, we use pthread_once for thread-safe initialization. + */ +#if defined(_WIN32) +/* SRWLOCK is statically initialized to zero (SRWLOCK_INIT) automatically */ +static js_mutex_t g_allocator_mutex = SRWLOCK_INIT; +#else +static js_mutex_t g_allocator_mutex; +static js_once_t g_allocator_once = JS_ONCE_INIT; + +static void init_allocator_mutex_once(void) { + JS_MUTEX_INIT(&g_allocator_mutex); +} +#endif + +/* Ensure allocator mutex is initialized (thread-safe) */ +static void ensure_allocator_mutex_init(void) { +#if defined(_WIN32) + /* SRWLOCK is already initialized statically - nothing to do */ + (void)0; +#else + pthread_once(&g_allocator_once, init_allocator_mutex_once); +#endif +} + +/* Public functions for mutex lifecycle management */ +void js_init_allocator_mutex(void) { + ensure_allocator_mutex_init(); +} + +void js_destroy_allocator_mutex(void) { +#if defined(_WIN32) + /* SRWLOCK does not need destruction */ + (void)0; +#else + /* Note: We cannot safely reset g_allocator_once after destruction + * because pthread_once_t is designed for one-time initialization. + * After js_cleanup() is called, the library should not be used again + * without a program restart. */ + JS_MUTEX_DESTROY(&g_allocator_mutex); +#endif +} + /* ============================================================================ * Allocator Functions * ============================================================================ */ void js_set_allocator(js_allocator_t alloc) { + ensure_allocator_mutex_init(); + + JS_MUTEX_LOCK(&g_allocator_mutex); + if (alloc.malloc && alloc.free) { g_allocator = alloc; /* Configure cJSON to use our allocator */ @@ -56,13 +135,19 @@ void js_set_allocator(js_allocator_t alloc) { g_allocator.user_data = NULL; cJSON_InitHooks(NULL); } + + JS_MUTEX_UNLOCK(&g_allocator_mutex); } js_allocator_t js_get_allocator(void) { + /* Reading the allocator struct - on modern platforms this is atomic enough + * for the expected use case. The allocator should only be set during init. */ return g_allocator; } void* js_malloc(size_t size) { + /* Note: The allocator functions (malloc/free) are expected to be thread-safe. + * We only lock when changing the allocator itself. */ return g_allocator.malloc(size); } diff --git a/c/tests/CMakeLists.txt b/c/tests/CMakeLists.txt index 4e7a745..0c1ff49 100644 --- a/c/tests/CMakeLists.txt +++ b/c/tests/CMakeLists.txt @@ -7,10 +7,17 @@ add_executable(json_structure_tests test_instance_validator.c test_conformance.c test_assets.c + test_thread_safety.c main.c ) target_link_libraries(json_structure_tests PRIVATE json_structure) +# Link pthread on Unix systems for thread safety tests +if(NOT WIN32) + find_package(Threads REQUIRED) + target_link_libraries(json_structure_tests PRIVATE Threads::Threads) +endif() + # Add tests add_test(NAME json_structure_tests COMMAND json_structure_tests) diff --git a/c/tests/main.c b/c/tests/main.c index 7057951..2779265 100644 --- a/c/tests/main.c +++ b/c/tests/main.c @@ -14,6 +14,7 @@ extern int test_schema_validator(void); extern int test_instance_validator(void); extern int test_conformance(void); extern int test_assets(void); +extern int test_thread_safety(void); int main(int argc, char* argv[]) { (void)argc; @@ -54,6 +55,12 @@ int main(int argc, char* argv[]) { total++; printf("Test Assets: %s\n\n", assets_failed == 0 ? "PASSED" : "FAILED"); + printf("Running thread safety tests...\n"); + int thread_failed = test_thread_safety(); + failed += thread_failed; + total++; + printf("Thread Safety: %s\n\n", thread_failed == 0 ? "PASSED" : "FAILED"); + printf("=== Summary ===\n"); printf("Total: %d, Passed: %d, Failed: %d\n", total, total - failed, failed); diff --git a/c/tests/test_thread_safety.c b/c/tests/test_thread_safety.c new file mode 100644 index 0000000..a287044 --- /dev/null +++ b/c/tests/test_thread_safety.c @@ -0,0 +1,368 @@ +/** + * @file test_thread_safety.c + * @brief Thread safety tests for JSON Structure C SDK + * + * Tests concurrent validation operations to ensure thread-safe behavior. + */ + +#include "json_structure/json_structure.h" +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#include +typedef HANDLE thread_t; +typedef unsigned (__stdcall *thread_func_t)(void*); +/* _beginthreadex returns handle on success (non-zero), 0 on failure. + * We want to return 0 on success, non-zero on failure for consistency with pthread. */ +static inline int win_thread_create(thread_t* t, thread_func_t f, void* a) { + *t = (HANDLE)_beginthreadex(NULL, 0, f, a, 0, NULL); + return (*t == NULL) ? 1 : 0; +} +#define thread_create(t, f, a) win_thread_create(t, (thread_func_t)(f), a) +#define thread_join(t) (WaitForSingleObject(t, INFINITE), CloseHandle(t)) +#define thread_return_t unsigned +#define THREAD_RETURN(x) return (x) +#else +#include +typedef pthread_t thread_t; +typedef void* (*thread_func_t)(void*); +#define thread_create(t, f, a) pthread_create(t, NULL, f, a) +#define thread_join(t) pthread_join(t, NULL) +#define thread_return_t void* +#define THREAD_RETURN(x) return (void*)(intptr_t)(x) +#endif + +#define TEST(name) static int test_##name(void) +#define RUN_TEST(name) do { \ + printf(" " #name "... "); \ + if (test_##name() == 0) { \ + printf("OK\n"); \ + } else { \ + printf("FAILED\n"); \ + failed++; \ + } \ +} while(0) + +/* ============================================================================ + * Test Data + * ============================================================================ */ + +static const char* test_schema = + "{" + " \"type\": \"object\"," + " \"properties\": {" + " \"name\": {\"type\": \"string\", \"minLength\": 1}," + " \"age\": {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 150}," + " \"email\": {\"type\": \"string\", \"pattern\": \"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$\"}" + " }," + " \"required\": [\"name\", \"age\"]" + "}"; + +static const char* test_instance_valid = + "{" + " \"name\": \"John Doe\"," + " \"age\": 30," + " \"email\": \"john.doe@example.com\"" + "}"; + +static const char* test_instance_invalid = + "{" + " \"name\": \"\"," + " \"age\": -5" + "}"; + +/* ============================================================================ + * Thread Test Functions + * ============================================================================ */ + +typedef struct { + int thread_id; + int iterations; + int failures; +} thread_data_t; + +static thread_return_t validate_schema_thread(void* arg) { + thread_data_t* data = (thread_data_t*)arg; + + for (int i = 0; i < data->iterations; i++) { + js_result_t result; + js_result_init(&result); + + if (!js_validate_schema(test_schema, &result)) { + data->failures++; + } + + js_result_cleanup(&result); + } + + THREAD_RETURN(0); +} + +static thread_return_t validate_instance_thread(void* arg) { + thread_data_t* data = (thread_data_t*)arg; + + for (int i = 0; i < data->iterations; i++) { + js_result_t result; + js_result_init(&result); + + /* Alternate between valid and invalid instances */ + const char* instance = (i % 2 == 0) ? test_instance_valid : test_instance_invalid; + bool expected_valid = (i % 2 == 0); + + bool is_valid = js_validate_instance(instance, test_schema, &result); + + if (is_valid != expected_valid) { + data->failures++; + } + + js_result_cleanup(&result); + } + + THREAD_RETURN(0); +} + +static thread_return_t mixed_validation_thread(void* arg) { + thread_data_t* data = (thread_data_t*)arg; + + for (int i = 0; i < data->iterations; i++) { + js_result_t result; + js_result_init(&result); + + /* Alternate between schema and instance validation */ + if (i % 2 == 0) { + if (!js_validate_schema(test_schema, &result)) { + data->failures++; + } + } else { + js_validate_instance(test_instance_valid, test_schema, &result); + } + + js_result_cleanup(&result); + } + + THREAD_RETURN(0); +} + +/* ============================================================================ + * Tests + * ============================================================================ */ + +TEST(concurrent_schema_validation) { + const int num_threads = 4; + const int iterations = 100; + thread_t threads[4]; + thread_data_t thread_data[4]; + + /* Create threads */ + for (int i = 0; i < num_threads; i++) { + thread_data[i].thread_id = i; + thread_data[i].iterations = iterations; + thread_data[i].failures = 0; + + if (thread_create(&threads[i], validate_schema_thread, &thread_data[i]) != 0) { + printf("Failed to create thread %d\n", i); + return 1; + } + } + + /* Wait for all threads */ + for (int i = 0; i < num_threads; i++) { + thread_join(threads[i]); + } + + /* Check for failures */ + int total_failures = 0; + for (int i = 0; i < num_threads; i++) { + total_failures += thread_data[i].failures; + } + + if (total_failures > 0) { + printf("Total validation failures: %d\n", total_failures); + return 1; + } + + return 0; +} + +TEST(concurrent_instance_validation) { + const int num_threads = 4; + const int iterations = 100; + thread_t threads[4]; + thread_data_t thread_data[4]; + + /* Create threads */ + for (int i = 0; i < num_threads; i++) { + thread_data[i].thread_id = i; + thread_data[i].iterations = iterations; + thread_data[i].failures = 0; + + if (thread_create(&threads[i], validate_instance_thread, &thread_data[i]) != 0) { + printf("Failed to create thread %d\n", i); + return 1; + } + } + + /* Wait for all threads */ + for (int i = 0; i < num_threads; i++) { + thread_join(threads[i]); + } + + /* Check for failures */ + int total_failures = 0; + for (int i = 0; i < num_threads; i++) { + total_failures += thread_data[i].failures; + } + + if (total_failures > 0) { + printf("Total validation failures: %d\n", total_failures); + return 1; + } + + return 0; +} + +TEST(concurrent_mixed_validation) { + const int num_threads = 8; + const int iterations = 50; + thread_t threads[8]; + thread_data_t thread_data[8]; + + /* Create threads */ + for (int i = 0; i < num_threads; i++) { + thread_data[i].thread_id = i; + thread_data[i].iterations = iterations; + thread_data[i].failures = 0; + + if (thread_create(&threads[i], mixed_validation_thread, &thread_data[i]) != 0) { + printf("Failed to create thread %d\n", i); + return 1; + } + } + + /* Wait for all threads */ + for (int i = 0; i < num_threads; i++) { + thread_join(threads[i]); + } + + /* Check for failures */ + int total_failures = 0; + for (int i = 0; i < num_threads; i++) { + total_failures += thread_data[i].failures; + } + + if (total_failures > 0) { + printf("Total validation failures: %d\n", total_failures); + return 1; + } + + return 0; +} + +TEST(allocator_concurrent_access) { + /* Test that memory allocation is thread-safe */ + const int num_threads = 4; + const int iterations = 1000; + thread_t threads[4]; + thread_data_t thread_data[4]; + + /* Create threads that perform lots of allocations */ + for (int i = 0; i < num_threads; i++) { + thread_data[i].thread_id = i; + thread_data[i].iterations = iterations; + thread_data[i].failures = 0; + + if (thread_create(&threads[i], validate_instance_thread, &thread_data[i]) != 0) { + printf("Failed to create thread %d\n", i); + return 1; + } + } + + /* Wait for all threads */ + for (int i = 0; i < num_threads; i++) { + thread_join(threads[i]); + } + + /* Check for failures */ + int total_failures = 0; + for (int i = 0; i < num_threads; i++) { + total_failures += thread_data[i].failures; + } + + if (total_failures > 0) { + printf("Total validation failures: %d\n", total_failures); + return 1; + } + + return 0; +} + +TEST(init_cleanup_lifecycle) { + /* Test that init/cleanup works correctly */ + + /* First initialization */ + js_init(); + + /* Validate something */ + js_result_t result; + js_result_init(&result); + + if (!js_validate_schema(test_schema, &result)) { + js_result_cleanup(&result); + return 1; + } + + js_result_cleanup(&result); + + /* Cleanup */ + js_cleanup(); + + /* Re-initialize */ + js_init(); + + /* Validate again */ + js_result_init(&result); + + if (!js_validate_schema(test_schema, &result)) { + js_result_cleanup(&result); + return 1; + } + + js_result_cleanup(&result); + + /* Final cleanup */ + js_cleanup(); + + return 0; +} + +/* ============================================================================ + * Test Suite Entry Point + * ============================================================================ */ + +int test_thread_safety(void) { + int failed = 0; + + /* Initialize library once before all tests */ + js_init(); + + printf("\n=== Thread Safety Tests ===\n"); + + RUN_TEST(concurrent_schema_validation); + RUN_TEST(concurrent_instance_validation); + RUN_TEST(concurrent_mixed_validation); + RUN_TEST(allocator_concurrent_access); + + /* Cleanup before lifecycle test (which does its own init/cleanup) */ + js_cleanup(); + + RUN_TEST(init_cleanup_lifecycle); + + /* Re-initialize for any subsequent tests */ + js_init(); + + return failed; +}