Linux HTTP implementation

This commit is contained in:
2026-04-15 15:11:44 -05:00
parent 6aff98d555
commit 133685ea37
13 changed files with 600 additions and 6 deletions
+3
View File
@@ -1,6 +1,7 @@
# Find link platform-specific libraries # Find link platform-specific libraries
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(CURL REQUIRED)
# Setup endianess at compile time to optimize. # Setup endianess at compile time to optimize.
include(TestBigEndian) include(TestBigEndian)
@@ -22,6 +23,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
OpenGL::GL OpenGL::GL
GL GL
m m
CURL::libcurl
) )
# Define platform-specific macros. # Define platform-specific macros.
@@ -38,4 +40,5 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
DUSK_INPUT_POINTER DUSK_INPUT_POINTER
DUSK_INPUT_GAMEPAD DUSK_INPUT_GAMEPAD
DUSK_TIME_DYNAMIC DUSK_TIME_DYNAMIC
THREAD_PTHREAD=1
) )
+2 -4
View File
@@ -74,8 +74,6 @@ add_subdirectory(scene)
add_subdirectory(script) add_subdirectory(script)
add_subdirectory(time) add_subdirectory(time)
add_subdirectory(ui) add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(util) add_subdirectory(util)
add_subdirectory(thread)
# if(DUSK_TARGET_SYSTEM STREQUAL "linux" OR DUSK_TARGET_SYSTEM STREQUAL "psp")
# add_subdirectory(thread)
# endif()
+30
View File
@@ -20,6 +20,9 @@
#include "entity/component/physics/entityphysics.h" #include "entity/component/physics/entityphysics.h"
#include "game/game.h" #include "game/game.h"
#include "physics/physicsmanager.h" #include "physics/physicsmanager.h"
#include "network/networkmanager.h"
#include "network/networkhttprequest.h"
#include "display/mesh/cube.h" #include "display/mesh/cube.h"
#include "display/mesh/plane.h" #include "display/mesh/plane.h"
@@ -29,6 +32,22 @@ engine_t ENGINE;
static entityid_t phBoxEnt; static entityid_t phBoxEnt;
static componentid_t phBoxPhys; static componentid_t phBoxPhys;
/* ---- test network callbacks ---- */
static void onTestResponse(
const uint16_t status,
const char_t *body,
const networkhttpheader_t *responseHeaders,
const uint32_t responseHeaderCount,
void *user
) {
printf("HTTP %d: %s\n", status, body);
}
static void onTestError(errorret_t error, void *user) {
printf("HTTP error: %s\n", error.state->message);
}
errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineInit(const int32_t argc, const char_t **argv) {
memoryZero(&ENGINE, sizeof(engine_t)); memoryZero(&ENGINE, sizeof(engine_t));
ENGINE.running = true; ENGINE.running = true;
@@ -46,8 +65,17 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(sceneInit()); errorChain(sceneInit());
entityManagerInit(); entityManagerInit();
physicsManagerInit(); physicsManagerInit();
errorChain(networkManagerInit());
errorChain(gameInit()); errorChain(gameInit());
networkHTTPRequest(
"http://localhost:3000/test",
NETWORK_HTTP_REQUEST_METHOD_GET,
NULL, NULL, 0, NULL,
onTestResponse,
onTestError
);
/* ---- Camera ---- */ /* ---- Camera ---- */
entityid_t cam = entityManagerAdd(); entityid_t cam = entityManagerAdd();
componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION); componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION);
@@ -107,6 +135,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorret_t engineUpdate(void) { errorret_t engineUpdate(void) {
timeUpdate(); timeUpdate();
inputUpdate(); inputUpdate();
errorChain(networkManagerUpdate());
uiUpdate(); uiUpdate();
errorChain(sceneUpdate()); errorChain(sceneUpdate());
@@ -134,6 +163,7 @@ void engineExit(void) {
} }
errorret_t engineDispose(void) { errorret_t engineDispose(void) {
errorChain(networkManagerDispose());
sceneDispose(); sceneDispose();
errorChain(gameDispose()); errorChain(gameDispose());
entityManagerDispose(); entityManagerDispose();
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networkmanager.c
networkhttprequest.c
)
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttprequest.h"
#include "assert/assert.h"
#include "network/networkplatform.h"
void networkHTTPRequest(
const char_t *url,
const networkhttprequestmethod_t method,
const char_t *bodyOrNull,
const networkhttpheader_t *headers,
const uint32_t headerCount,
void *user,
networkhttpcallback_t callback,
networkhttperrorcallback_t errorCallback
) {
assertStrLenMin(url, 1, "URL must be non-empty");
assertNotNull(callback, "Callback must be non-NULL");
assertNotNull(errorCallback, "Error callback must be non-NULL");
assertTrue(headerCount == 0 || headers != NULL, "Headers must not be NULL when headerCount > 0");
uint32_t count = (headers == NULL) ? 0 : headerCount;
networkPlatformHTTPRequest(
url, method, bodyOrNull, headers, count, user, callback, errorCallback
);
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef enum {
NETWORK_HTTP_REQUEST_METHOD_GET,
NETWORK_HTTP_REQUEST_METHOD_POST,
NETWORK_HTTP_REQUEST_METHOD_PUT,
NETWORK_HTTP_REQUEST_METHOD_DELETE,
NETWORK_HTTP_REQUEST_METHOD_HEAD,
NETWORK_HTTP_REQUEST_METHOD_OPTIONS,
} networkhttprequestmethod_t;
typedef struct {
const char_t *key;
const char_t *value;
} networkhttpheader_t;
typedef void (*networkhttpcallback_t)(
const uint16_t status,
const char_t *body,
const networkhttpheader_t *responseHeaders,
const uint32_t responseHeaderCount,
void *user
);
typedef void (*networkhttperrorcallback_t)(
errorret_t error,
void *user
);
/**
* Makes a HTTP Request. The request itself is asynchronous but the callback
* will be invoked on the main thread when next available.
*
* @param url URL to request.
* @param method Method to use for the request.
* @param bodyOrNull If POST or PUT, custom body string, can be NULL.
* @param headers Array of key-value headers.
* @param headerCount Count of headers, can be anything if headers is NULL.
* @param user Callback pointer received to the callback.
* @param callback The callback to invoke when the request completes.
* @param errorCallback The callback to invoke if the request fails.
* Note, this doesn't count Non-200 status codes, just
* network errors.
*/
void networkHTTPRequest(
const char_t *url,
const networkhttprequestmethod_t method,
const char_t *bodyOrNull,
const networkhttpheader_t *headers,
const uint32_t headerCount,
void *user,
networkhttpcallback_t callback,
networkhttperrorcallback_t errorCallback
);
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkmanager.h"
#include "util/memory.h"
#include "network/networkplatform.h"
networkmanager_t NETWORK_MANAGER;
errorret_t networkManagerInit() {
memoryZero(&NETWORK_MANAGER, sizeof(networkmanager_t));
errorChain(networkPlatformInit());
errorOk();
}
errorret_t networkManagerUpdate() {
errorChain(networkPlatformUpdate());
errorOk();
}
errorret_t networkManagerDispose() {
errorChain(networkPlatformDispose());
errorOk();
}
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
void *nothing;
} networkmanager_t;
extern networkmanager_t NETWORK_MANAGER;
/**
* Initializes the network manager. This will NOT connect to the network.
*
* @return An error code indicating success or failure.
*/
errorret_t networkManagerInit();
/**
* Updates the network manager, dispatching any completed async request
* callbacks on the main thread.
*
* @return An error code indicating success or failure.
*/
errorret_t networkManagerUpdate();
/**
* Disposes of the network manager. This will NOT disconnect from the network.
*
* @return An error code indicating success or failure.
*/
errorret_t networkManagerDispose();
+1
View File
@@ -13,3 +13,4 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME}
add_subdirectory(asset) add_subdirectory(asset)
add_subdirectory(log) add_subdirectory(log)
add_subdirectory(input) add_subdirectory(input)
add_subdirectory(network)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networklinux.c
)
+272
View File
@@ -0,0 +1,272 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networklinux.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include <curl/curl.h>
static networklinux_t NETWORK_LINUX;
static size_t networkLinuxCurlWrite(
char *ptr,
size_t size,
size_t nmemb,
void *userdata
) {
assertNotNull(ptr, "curl write ptr must not be NULL");
assertNotNull(userdata, "curl write userdata must not be NULL");
networkhttppendingitem_t *item = (networkhttppendingitem_t *)userdata;
size_t incoming = size * nmemb;
size_t written = strlen(item->responseBody);
size_t space = NETWORK_HTTP_RESPONSE_MAX - 1 - written;
if(incoming > space) incoming = space;
if(incoming > 0) {
memcpy(item->responseBody + written, ptr, incoming);
item->responseBody[written + incoming] = '\0';
}
/* Return the original byte count — curl treats anything else as an error. */
return size * nmemb;
}
static void networkLinuxHTTPThread(thread_t *thread) {
assertNotNull(thread, "Thread must not be NULL");
assertNotNull(thread->data, "Thread data must not be NULL");
networkhttppendingitem_t *item = (networkhttppendingitem_t *)thread->data;
CURL *curl = curl_easy_init();
if(!curl) {
stringCopy(
item->errorMessage, "curl_easy_init failed", NETWORK_ERROR_MESSAGE_MAX - 1
);
item->isError = true;
goto done;
}
curl_easy_setopt(curl, CURLOPT_URL, item->url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, networkLinuxCurlWrite);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, item);
switch(item->method) {
case NETWORK_HTTP_REQUEST_METHOD_POST:
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(
curl, CURLOPT_POSTFIELDS, item->hasBody ? item->body : ""
);
curl_easy_setopt(
curl, CURLOPT_POSTFIELDSIZE,
(long)(item->hasBody ? strlen(item->body) : 0)
);
break;
case NETWORK_HTTP_REQUEST_METHOD_PUT:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
if(item->hasBody) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, item->body);
curl_easy_setopt(
curl, CURLOPT_POSTFIELDSIZE, (long)strlen(item->body)
);
}
break;
case NETWORK_HTTP_REQUEST_METHOD_DELETE:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
break;
case NETWORK_HTTP_REQUEST_METHOD_HEAD:
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
break;
case NETWORK_HTTP_REQUEST_METHOD_OPTIONS:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
break;
case NETWORK_HTTP_REQUEST_METHOD_GET:
default:
break;
}
/* Build header list */
struct curl_slist *curlHeaders = NULL;
if(item->headerCount > 0) {
char_t line[NETWORK_HTTP_HEADER_KEY_MAX + NETWORK_HTTP_HEADER_VAL_MAX + 3];
for(uint32_t i = 0; i < item->headerCount; i++) {
snprintf(
line, sizeof(line), "%s: %s", item->headerKeys[i], item->headerVals[i]
);
curlHeaders = curl_slist_append(curlHeaders, line);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlHeaders);
}
CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK) {
stringCopy(
item->errorMessage, curl_easy_strerror(res), NETWORK_ERROR_MESSAGE_MAX - 1
);
item->isError = true;
} else {
long code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
item->status = (uint16_t)code;
item->isError = false;
}
if(curlHeaders) curl_slist_free_all(curlHeaders);
curl_easy_cleanup(curl);
done:
threadMutexLock(&NETWORK_LINUX.resultsMutex);
item->resultReady = true;
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
}
errorret_t networkLinuxInit() {
memoryZero(&NETWORK_LINUX, sizeof(networklinux_t));
threadMutexInit(&NETWORK_LINUX.resultsMutex);
CURLcode res = curl_global_init(CURL_GLOBAL_ALL);
if(res != CURLE_OK) {
errorThrow("curl_global_init failed: %s", curl_easy_strerror(res));
}
errorOk();
}
errorret_t networkLinuxUpdate() {
for(int32_t i = 0; i < NETWORK_HTTP_PENDING_MAX; i++) {
/* Check under lock whether this slot has a result waiting. */
threadMutexLock(&NETWORK_LINUX.resultsMutex);
networkhttppendingitem_t *item = &NETWORK_LINUX.requests[i];
if(!item->used || !item->resultReady) {
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
continue;
}
/* Snapshot the small values we need after we drop the lock. */
bool_t isError = item->isError;
uint16_t status = item->status;
networkhttpcallback_t cb = item->callback;
networkhttperrorcallback_t errCb = item->errorCallback;
void *user = item->user;
/*
* Keep pointers into item's string buffers — valid because item->used is
* still true here and only this (main) thread clears it.
*/
const char_t *responseBody = item->responseBody;
const char_t *errorMessage = item->errorMessage;
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
/* Fire the callback without holding the lock. */
if(isError) {
errorstate_t errState;
errState.code = ERROR_NOT_OK;
errState.message = (char_t *)errorMessage;
errState.lines = (char_t *)"";
errorret_t err;
err.code = ERROR_NOT_OK;
err.state = &errState;
errCb(err, user);
} else {
cb(status, responseBody, NULL, 0, user);
}
/* Release the slot now that the callback has consumed it. */
threadMutexLock(&NETWORK_LINUX.resultsMutex);
item->used = false;
item->resultReady = false;
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
}
errorOk();
}
errorret_t networkLinuxDispose() {
curl_global_cleanup();
threadMutexDispose(&NETWORK_LINUX.resultsMutex);
errorOk();
}
void networkLinuxHTTPRequest(
const char_t *url,
const networkhttprequestmethod_t method,
const char_t *bodyOrNull,
const networkhttpheader_t *headers,
const uint32_t headerCount,
void *user,
networkhttpcallback_t callback,
networkhttperrorcallback_t errorCallback
) {
assertStrLenMin(url, 1, "URL must be non-empty");
assertStrLenMax(url, NETWORK_HTTP_URL_MAX, "URL exceeds maximum length");
assertNotNull(callback, "Callback must be non-NULL");
assertNotNull(errorCallback, "Error callback must be non-NULL");
assertTrue(headerCount == 0 || headers != NULL, "Headers must not be NULL when headerCount > 0");
/* Allocate a slot. */
threadMutexLock(&NETWORK_LINUX.resultsMutex);
networkhttppendingitem_t *item = NULL;
for(int32_t i = 0; i < NETWORK_HTTP_PENDING_MAX; i++) {
if(!NETWORK_LINUX.requests[i].used) {
item = &NETWORK_LINUX.requests[i];
break;
}
}
if(!item) {
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
errorstate_t errState;
errState.code = ERROR_NOT_OK;
errState.message = (char_t *)"No free HTTP request slots";
errState.lines = (char_t *)"";
errorret_t err;
err.code = ERROR_NOT_OK;
err.state = &errState;
errorCallback(err, user);
return;
}
memoryZero(item, sizeof(networkhttppendingitem_t));
item->used = true;
threadMutexUnlock(&NETWORK_LINUX.resultsMutex);
/* Fill the slot (safe: only we have access while used=true, resultReady=false). */
stringCopy(item->url, url, NETWORK_HTTP_URL_MAX - 1);
item->method = method;
if(bodyOrNull != NULL) {
assertStrLenMax(bodyOrNull, NETWORK_HTTP_BODY_MAX, "Body exceeds maximum length");
stringCopy(item->body, bodyOrNull, NETWORK_HTTP_BODY_MAX - 1);
item->hasBody = true;
}
uint32_t hdrCount = headerCount;
if(hdrCount > NETWORK_HTTP_HEADER_MAX) hdrCount = NETWORK_HTTP_HEADER_MAX;
item->headerCount = hdrCount;
for(uint32_t i = 0; i < hdrCount; i++) {
assertStrLenMax(headers[i].key, NETWORK_HTTP_HEADER_KEY_MAX, "Header key exceeds maximum length");
assertStrLenMax(headers[i].value, NETWORK_HTTP_HEADER_VAL_MAX, "Header value exceeds maximum length");
stringCopy(item->headerKeys[i], headers[i].key, NETWORK_HTTP_HEADER_KEY_MAX - 1);
stringCopy(item->headerVals[i], headers[i].value, NETWORK_HTTP_HEADER_VAL_MAX - 1);
}
item->callback = callback;
item->errorCallback = errorCallback;
item->user = user;
/* Kick off the background thread. */
threadInit(&item->thread, networkLinuxHTTPThread);
item->thread.data = item;
threadStartRequest(&item->thread);
}
+98
View File
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#include "network/networkhttprequest.h"
#include "thread/thread.h"
#define NETWORK_HTTP_PENDING_MAX 16
#define NETWORK_HTTP_URL_MAX 512
#define NETWORK_HTTP_BODY_MAX 8192
#define NETWORK_HTTP_RESPONSE_MAX 65536
#define NETWORK_HTTP_HEADER_MAX 16
#define NETWORK_HTTP_HEADER_KEY_MAX 64
#define NETWORK_HTTP_HEADER_VAL_MAX 256
#define NETWORK_ERROR_MESSAGE_MAX 256
typedef struct {
bool_t used;
volatile bool_t resultReady;
bool_t isError;
char_t url[NETWORK_HTTP_URL_MAX];
networkhttprequestmethod_t method;
char_t body[NETWORK_HTTP_BODY_MAX];
bool_t hasBody;
char_t headerKeys[NETWORK_HTTP_HEADER_MAX][NETWORK_HTTP_HEADER_KEY_MAX];
char_t headerVals[NETWORK_HTTP_HEADER_MAX][NETWORK_HTTP_HEADER_VAL_MAX];
uint32_t headerCount;
networkhttpcallback_t callback;
networkhttperrorcallback_t errorCallback;
void *user;
uint16_t status;
char_t responseBody[NETWORK_HTTP_RESPONSE_MAX];
char_t errorMessage[NETWORK_ERROR_MESSAGE_MAX];
thread_t thread;
} networkhttppendingitem_t;
typedef struct {
networkhttppendingitem_t requests[NETWORK_HTTP_PENDING_MAX];
threadmutex_t resultsMutex;
} networklinux_t;
/**
* Initializes the network manager. Must be called before any other network
* functions.
*
* @return Any error that occurs.
*/
errorret_t networkLinuxInit();
/**
* Updates the network manager, called once per frame to handle completed
* HTTP requests.
*
* @return Any error that occurs.
*/
errorret_t networkLinuxUpdate();
/**
* Disposes the network manager.
*
* @return Any error that occurs.
*/
errorret_t networkLinuxDispose();
/**
* Submits an asynchronous HTTP request. The callback will be invoked on the
* main thread when next available.
*
* @param url URL to request.
* @param method Method to use for the request.
* @param bodyOrNull If POST or PUT, custom body string, can be NULL.
* @param headers Array of key-value headers.
* @param headerCount Count of headers, can be anything if headers is NULL.
* @param user Callback pointer received to the callback.
* @param callback The callback to invoke when the request completes.
* @param errorCallback The callback to invoke if the request fails.
* Note, this doesn't count Non-200 status codes, just
* network errors.
*/
void networkLinuxHTTPRequest(
const char_t *url,
const networkhttprequestmethod_t method,
const char_t *bodyOrNull,
const networkhttpheader_t *headers,
const uint32_t headerCount,
void *user,
networkhttpcallback_t callback,
networkhttperrorcallback_t errorCallback
);
+14
View File
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "networklinux.h"
#define networkPlatformInit networkLinuxInit
#define networkPlatformUpdate networkLinuxUpdate
#define networkPlatformDispose networkLinuxDispose
#define networkPlatformHTTPRequest networkLinuxHTTPRequest