Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 61 additions & 0 deletions c/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 25 additions & 2 deletions c/include/json_structure/json_structure.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions c/include/json_structure/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
17 changes: 16 additions & 1 deletion c/src/json_source_locator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/* ============================================================================
Expand Down
85 changes: 85 additions & 0 deletions c/src/types.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@
#include <string.h>
#include <stdio.h>

/* 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 <windows.h>
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 <pthread.h>
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
* ============================================================================ */
Expand All @@ -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 */
Expand All @@ -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);
}

Expand Down
7 changes: 7 additions & 0 deletions c/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 7 additions & 0 deletions c/tests/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading