Dqn/dqn_os_posix.cpp

971 lines
34 KiB
C++
Raw Normal View History

////////////////////////////////////////////////////////////////////////////////////////////////////
//
// $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\
// $$ __$$\ $$ __$$\ $$ __$$\ $$ __$$\ $$ __$$\ \_$$ _|$$ | $$ |
// $$ / $$ |$$ / \__| $$ | $$ |$$ / $$ |$$ / \__| $$ | \$$\ $$ |
// $$ | $$ |\$$$$$$\ $$$$$$$ |$$ | $$ |\$$$$$$\ $$ | \$$$$ /
// $$ | $$ | \____$$\ $$ ____/ $$ | $$ | \____$$\ $$ | $$ $$<
// $$ | $$ |$$\ $$ | $$ | $$ | $$ |$$\ $$ | $$ | $$ /\$$\
// $$$$$$ |\$$$$$$ | $$ | $$$$$$ |\$$$$$$ |$$$$$$\ $$ / $$ |
// \______/ \______/ \__| \______/ \______/ \______|\__| \__|
//
// dqn_os_posix.cpp -- Posix implementation of the OS layer
//
////////////////////////////////////////////////////////////////////////////////////////////////////
// NOTE: [$VMEM] Dqn_OSMem //////////////////////////////////////////////////////////////////////////
static uint32_t Dqn_OS_MemConvertPageToOSFlags_(uint32_t protect)
{
DQN_ASSERT((protect & ~Dqn_OSMemPage_All) == 0);
DQN_ASSERT(protect != 0);
uint32_t result = 0;
if (protect & (Dqn_OSMemPage_NoAccess | Dqn_OSMemPage_Guard)) {
result = PROT_NONE;
} else {
if (protect & Dqn_OSMemPage_Read)
result = PROT_READ;
if (protect & Dqn_OSMemPage_Write)
result = PROT_WRITE;
}
return result;
}
DQN_API void *Dqn_OS_MemReserve(Dqn_usize size, Dqn_OSMemCommit commit, uint32_t page_flags)
{
unsigned long os_page_flags = Dqn_OS_MemConvertPageToOSFlags_(page_flags);
if (commit == Dqn_OSMemCommit_Yes)
os_page_flags |= (PROT_READ | PROT_WRITE);
void *result = mmap(nullptr, size, os_page_flags, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (result == MAP_FAILED)
result = nullptr;
return result;
}
DQN_API bool Dqn_OS_MemCommit(void *ptr, Dqn_usize size, uint32_t page_flags)
{
bool result = false;
if (!ptr || size == 0)
return false;
unsigned long os_page_flags = Dqn_OS_MemConvertPageToOSFlags_(page_flags);
result = mprotect(ptr, size, os_page_flags) == 0;
return result;
}
DQN_API void Dqn_OS_MemDecommit(void *ptr, Dqn_usize size)
{
mprotect(ptr, size, PROT_NONE);
madvise(ptr, size, MADV_FREE);
}
DQN_API void Dqn_OS_MemRelease(void *ptr, Dqn_usize size)
{
munmap(ptr, size);
}
DQN_API int Dqn_OS_MemProtect(void *ptr, Dqn_usize size, uint32_t page_flags)
{
if (!ptr || size == 0)
return 0;
static Dqn_Str8 const ALIGNMENT_ERROR_MSG =
DQN_STR8("Page protection requires pointers to be page aligned because we "
"can only guard memory at a multiple of the page boundary.");
DQN_ASSERTF(Dqn_IsPowerOfTwoAligned(DQN_CAST(uintptr_t)ptr, g_dqn_library->os_page_size), "%s", ALIGNMENT_ERROR_MSG.data);
DQN_ASSERTF(Dqn_IsPowerOfTwoAligned(size, g_dqn_library->os_page_size), "%s", ALIGNMENT_ERROR_MSG.data);
unsigned long os_page_flags = Dqn_OS_MemConvertPageToOSFlags_(page_flags);
int result = mprotect(ptr, size, os_page_flags);
DQN_ASSERTF(result == 0, "mprotect failed (%d)", errno);
return result;
}
// NOTE: [$DATE] Date //////////////////////////////////////////////////////////////////////////////
DQN_API Dqn_OSDateTime Dqn_OS_DateLocalTimeNow()
{
Dqn_OSDateTime result = {};
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
// NOTE: localtime_r is used because it is thread safe
// See: https://linux.die.net/man/3/localtime
// According to POSIX.1-2004, localtime() is required to behave as though
// tzset(3) was called, while localtime_r() does not have this requirement.
// For portable code tzset(3) should be called before localtime_r().
for (static bool once = true; once; once = false)
tzset();
struct tm time = {};
localtime_r(&ts.tv_sec, &time);
result.hour = time.tm_hour;
result.minutes = time.tm_min;
result.seconds = time.tm_sec;
result.day = DQN_CAST(uint8_t)time.tm_mday;
result.month = DQN_CAST(uint8_t)time.tm_mon + 1;
result.year = 1900 + DQN_CAST(int16_t)time.tm_year;
return result;
}
DQN_API uint64_t Dqn_OS_DateUnixTime()
{
uint64_t result = time(nullptr);
return result;
}
DQN_API uint64_t Dqn_OS_DateToUnixTime(Dqn_OSDateTime date)
{
DQN_ASSERT(Dqn_OS_DateIsValid(date));
struct tm timeinfo = {};
timeinfo.tm_year = date.year - 1900;
timeinfo.tm_mon = date.month - 1;
timeinfo.tm_mday = date.day;
timeinfo.tm_hour = date.hour;
timeinfo.tm_min = date.minutes;
timeinfo.tm_sec = date.seconds;
uint64_t result = mktime(&timeinfo);
return result;
}
DQN_API bool Dqn_OS_SecureRNGBytes(void *buffer, uint32_t size)
{
2024-01-31 13:03:06 +00:00
#if defined(DQN_PLATFORM_EMSCRIPTEN)
(void)buffer; (void)size;
return false;
#else
if (!buffer || size < 0)
return false;
if (size == 0)
return true;
DQN_ASSERTF(size <= 32,
"We can increase this by chunking the buffer and filling 32 bytes at a time. *Nix guarantees 32 "
"bytes can always be fulfilled by this system at a time");
// TODO(doyle): https://github.com/jedisct1/libsodium/blob/master/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c
// TODO(doyle): https://man7.org/linux/man-pages/man2/getrandom.2.html
uint32_t read_bytes = 0;
do {
read_bytes = getrandom(buffer, size, 0); // NOTE: EINTR can not be triggered if size <= 32 bytes
} while (read_bytes != size || errno == EAGAIN);
return true;
2024-01-31 13:03:06 +00:00
#endif
}
DQN_API Dqn_Str8 Dqn_OS_EXEPath(Dqn_Arena *arena)
{
Dqn_Str8 result = {};
if (!arena)
return result;
int required_size_wo_null_terminator = 0;
for (int try_size = 128;; try_size *= 2) {
auto scoped_arena = Dqn_ArenaTempMemScope(arena);
char *try_buf = Dqn_Arena_NewArray(arena, char, try_size, Dqn_ZeroMem_No);
int bytes_written = readlink("/proc/self/exe", try_buf, try_size);
if (bytes_written == -1) {
// Failed, we're unable to determine the executable directory
break;
} else if (bytes_written == try_size) {
// Try again, if returned size was equal- we may of prematurely
// truncated according to the man pages
continue;
} else {
// readlink will give us the path to the executable. Once we
// determine the correct buffer size required to get the full file
// path, we do some post-processing on said string and extract just
// the directory.
// TODO(dqn): It'd be nice if there's some way of keeping this
// try_buf around, memcopy the byte and trash the try_buf from the
// arena. Instead we just get the size and redo the call one last
// time after this "calculate" step.
DQN_ASSERTF(bytes_written < try_size, "bytes_written can never be greater than the try size, function writes at most try_size");
required_size_wo_null_terminator = bytes_written;
break;
}
}
if (required_size_wo_null_terminator) {
Dqn_ArenaTempMem temp_mem = Dqn_Arena_TempMemBegin(arena);
char *exe_path = Dqn_Arena_NewArray(arena, char, required_size_wo_null_terminator + 1, Dqn_ZeroMem_No);
exe_path[required_size_wo_null_terminator] = 0;
int bytes_written = readlink("/proc/self/exe", exe_path, required_size_wo_null_terminator);
if (bytes_written == -1) {
// Note that if read-link fails again can be because there's
// a potential race condition here, our exe or directory could have
// been deleted since the last call, so we need to be careful.
Dqn_Arena_TempMemEnd(temp_mem);
} else {
result = Dqn_Str8_Init(exe_path, required_size_wo_null_terminator);
}
}
return result;
}
DQN_API uint64_t Dqn_OS_PerfCounterFrequency()
{
// NOTE: On Linux we use clock_gettime(CLOCK_MONOTONIC_RAW) which
// increments at nanosecond granularity.
uint64_t result = 1'000'000'000;
return result;
}
DQN_API uint64_t Dqn_OS_PerfCounterNow()
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
uint64_t result = DQN_CAST(uint64_t) ts.tv_sec * 1'000'000'000 + DQN_CAST(uint64_t) ts.tv_nsec;
return result;
}
#if !defined(DQN_NO_OS_FILE_API)
DQN_API Dqn_OSPathInfo Dqn_OS_PathInfo(Dqn_Str8 path)
{
Dqn_OSPathInfo result = {};
if (!Dqn_Str8_HasData(path))
return result;
struct stat file_stat;
if (lstat(path.data, &file_stat) != -1) {
result.exists = true;
result.size = file_stat.st_size;
result.last_access_time_in_s = file_stat.st_atime;
result.last_write_time_in_s = file_stat.st_mtime;
// TODO(dqn): Seems linux does not support creation time via stat. We
// shoddily deal with this.
result.create_time_in_s = DQN_MIN(result.last_access_time_in_s, result.last_write_time_in_s);
}
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API bool Dqn_OS_PathDelete(Dqn_Str8 path)
{
bool result = false;
if (Dqn_Str8_HasData(path))
result = remove(path.data) == 0;
return result;
}
DQN_API bool Dqn_OS_FileExists(Dqn_Str8 path)
{
bool result = false;
if (!Dqn_Str8_HasData(path))
return result;
struct stat stat_result;
if (lstat(path.data, &stat_result) != -1)
result = S_ISREG(stat_result.st_mode) || S_ISLNK(stat_result.st_mode);
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API bool Dqn_OS_CopyFile(Dqn_Str8 src, Dqn_Str8 dest, bool overwrite, Dqn_ErrorSink *error)
{
bool result = false;
#if defined(DQN_PLATFORM_EMSCRIPTEN)
2024-02-11 07:23:13 +00:00
Dqn_ErrorSink_MakeF(error, 1, "Unsupported on Emscripten because of their VFS model");
#else
2024-02-11 07:23:13 +00:00
int src_fd = open(src.data, O_RDONLY);
if (src_fd == -1) {
int error_code = errno;
Dqn_ErrorSink_MakeF(error,
error_code,
"Failed to open file '%.*s' for copying: (%d) %s",
DQN_STR_FMT(src),
error_code,
strerror(error_code));
return result;
}
2024-02-11 07:23:13 +00:00
DQN_DEFER {
close(src_fd);
2024-02-11 07:23:13 +00:00
};
2024-02-11 07:23:13 +00:00
int dest_fd = open(dest.data, O_WRONLY | O_CREAT | (overwrite ? O_TRUNC : 0));
if (dest_fd == -1) {
int error_code = errno;
Dqn_ErrorSink_MakeF(error,
error_code,
"Failed to open file destination '%.*s' for copying to: (%d) %s",
DQN_STR_FMT(src),
error_code,
strerror(error_code));
return result;
}
DQN_DEFER {
close(dest_fd);
2024-02-11 07:23:13 +00:00
};
struct stat stat_existing;
int fstat_result = fstat(src_fd, &stat_existing);
if (fstat_result == -1) {
int error_code = errno;
Dqn_ErrorSink_MakeF(error,
error_code,
"Failed to query file size of '%.*s' for copying: (%d) %s",
DQN_STR_FMT(src),
error_code,
strerror(error_code));
return result;
}
ssize_t bytes_written = sendfile64(dest_fd, src_fd, 0, stat_existing.st_size);
result = (bytes_written == stat_existing.st_size);
if (!result) {
int error_code = errno;
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Str8 file_size_str8 = Dqn_U64ToByteSizeStr8(scratch.arena, stat_existing.st_size, Dqn_U64ByteSizeType_Auto);
Dqn_Str8 bytes_written_str8 = Dqn_U64ToByteSizeStr8(scratch.arena, bytes_written, Dqn_U64ByteSizeType_Auto);
Dqn_ErrorSink_MakeF(error,
error_code,
"Failed to copy file '%.*s' to '%.*s', we copied %.*s but the file size is %.*s: (%d) %s",
DQN_STR_FMT(src),
DQN_STR_FMT(dest),
DQN_STR_FMT(bytes_written_str8),
DQN_STR_FMT(file_size_str8),
error_code,
strerror(error_code));
}
#endif
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API bool Dqn_OS_MoveFile(Dqn_Str8 src, Dqn_Str8 dest, bool overwrite, Dqn_ErrorSink *error)
{
// See: https://github.com/gingerBill/gb/blob/master/gb.h
bool result = false;
bool file_moved = true;
if (link(src.data, dest.data) == -1) {
// NOTE: Link can fail if we're trying to link across different volumes
// so we fall back to a binary directory.
2024-02-11 07:23:13 +00:00
file_moved |= Dqn_OS_CopyFile(src, dest, overwrite, error);
}
2024-02-11 07:23:13 +00:00
if (file_moved) {
int unlink_result = unlink(src.data);
if (unlink_result == -1) {
int error_code = errno;
Dqn_ErrorSink_MakeF(error,
error_code,
"File '%.*s' was moved but failed to be unlinked from old location: (%d) %s",
DQN_STR_FMT(src),
error_code,
strerror(error_code));
}
}
return result;
}
DQN_API bool Dqn_OS_DirExists(Dqn_Str8 path)
{
bool result = false;
if (!Dqn_Str8_HasData(path))
return result;
struct stat stat_result;
if (lstat(path.data, &stat_result) != -1)
result = S_ISDIR(stat_result.st_mode);
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API bool Dqn_OS_MakeDir(Dqn_Str8 path)
{
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
bool result = true;
// TODO(doyle): Implement this without using the path indexes, it's not
// necessary. See Windows implementation.
Dqn_usize path_indexes_size = 0;
uint16_t path_indexes[64] = {};
Dqn_Str8 copy = Dqn_Str8_Copy(scratch.arena, path);
for (Dqn_usize index = copy.size - 1; index < copy.size; index--) {
bool first_char = index == (copy.size - 1);
char ch = copy.data[index];
if (ch == '/' || first_char) {
char temp = copy.data[index];
if (!first_char)
copy.data[index] = 0; // Temporarily null terminate it
bool is_file = Dqn_OS_FileExists(copy);
if (!first_char)
copy.data[index] = temp; // Undo null termination
if (is_file) {
// NOTE: There's something that exists in at this path, but
// it's not a directory. This request to make a directory is
// invalid.
return false;
} else {
if (Dqn_OS_DirExists(copy)) {
// NOTE: We found a directory, we can stop here and start
// building up all the directories that didn't exist up to
// this point.
break;
} else {
// NOTE: There's nothing that exists at this path, we can
// create a directory here
path_indexes[path_indexes_size++] = DQN_CAST(uint16_t)index;
}
}
}
}
for (Dqn_usize index = path_indexes_size - 1; result && index < path_indexes_size; index--) {
uint16_t path_index = path_indexes[index];
char temp = copy.data[path_index];
if (index != 0) copy.data[path_index] = 0;
result |= mkdir(copy.data, 0774) == 0;
if (index != 0) copy.data[path_index] = temp;
}
return result;
}
// NOTE: R/W Stream API ////////////////////////////////////////////////////////////////////////////
2024-02-11 07:23:13 +00:00
DQN_API Dqn_OSFile Dqn_OS_FileOpen(Dqn_Str8 path, Dqn_OSFileOpen open_mode, uint32_t access, Dqn_ErrorSink *error)
{
Dqn_OSFile result = {};
if (!Dqn_Str8_HasData(path) || path.size <= 0)
return result;
if ((access & ~Dqn_OSFileAccess_All) || ((access & Dqn_OSFileAccess_All) == 0)) {
DQN_INVALID_CODE_PATH;
return result;
}
if (access & Dqn_OSFileAccess_Execute) {
2024-02-11 07:23:13 +00:00
result.error = true;
Dqn_ErrorSink_MakeF(error, 1, "Failed to open file '%.*s': File access flag 'execute' is not supported", DQN_STR_FMT(path));
DQN_INVALID_CODE_PATH; // TODO: Not supported via fopen
return result;
}
// NOTE: fopen interface is not as expressive as the Win32
// We will fopen the file beforehand to setup the state/check for validity
2024-02-11 07:23:13 +00:00
// before closing and reopening it with the correct request access
// permissions.
{
FILE *handle = nullptr;
switch (open_mode) {
case Dqn_OSFileOpen_CreateAlways: handle = fopen(path.data, "w"); break;
case Dqn_OSFileOpen_OpenIfExist: handle = fopen(path.data, "r"); break;
case Dqn_OSFileOpen_OpenAlways: handle = fopen(path.data, "a"); break;
default: DQN_INVALID_CODE_PATH; break;
}
2024-02-11 07:23:13 +00:00
if (!handle) { // TODO(doyle): FileOpen flag to string
result.error = true;
Dqn_ErrorSink_MakeF(error, 1, "Failed to open file '%.*s': File could not be opened in requested mode 'Dqn_OSFileOpen' flag %d", DQN_STR_FMT(path), open_mode);
return result;
}
fclose(handle);
}
char const *fopen_mode = nullptr;
if (access & Dqn_OSFileAccess_AppendOnly) {
fopen_mode = "a+";
} else if (access & Dqn_OSFileAccess_Write) {
fopen_mode = "w+";
} else if (access & Dqn_OSFileAccess_Read) {
fopen_mode = "r+";
}
FILE *handle = fopen(path.data, fopen_mode);
if (!handle) {
2024-02-11 07:23:13 +00:00
result.error = true;
Dqn_ErrorSink_MakeF(error, 1, "Failed to open file '%.*s': File could not be opened with requested access mode 'Dqn_OSFileAccess' %d", DQN_STR_FMT(path), fopen_mode);
return result;
}
result.handle = handle;
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API bool Dqn_OS_FileRead(Dqn_OSFile *file, void *buffer, Dqn_usize size, Dqn_ErrorSink *error)
{
2024-02-11 07:23:13 +00:00
if (!file || !file->handle || file->error || !buffer || size <= 0)
return false;
if (fread(buffer, size, 1, DQN_CAST(FILE *)file->handle) != 1) {
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Str8 buffer_size_str8 = Dqn_U64ToByteSizeStr8(scratch.arena, size, Dqn_U64ByteSizeType_Auto);
Dqn_ErrorSink_MakeF(error, 1, "Failed to read %.*s from file", DQN_STR_FMT(buffer_size_str8));
return false;
}
return true;
}
DQN_API bool Dqn_OS_FileWritePtr(Dqn_OSFile *file, void const *buffer, Dqn_usize size, Dqn_ErrorSink *error)
{
if (!file || !file->handle || file->error || !buffer || size <= 0)
return false;
bool result = fwrite(buffer, DQN_CAST(Dqn_usize)size, 1 /*count*/, DQN_CAST(FILE *)file->handle) == 1 /*count*/;
2024-02-11 07:23:13 +00:00
if (!result) {
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
Dqn_Str8 buffer_size_str8 = Dqn_U64ToByteSizeStr8(scratch.arena, size, Dqn_U64ByteSizeType_Auto);
Dqn_ErrorSink_MakeF(error, 1, "Failed to write buffer (%s) to file handle", DQN_STR_FMT(buffer_size_str8));
}
return result;
}
2024-02-11 07:23:13 +00:00
DQN_API void Dqn_OS_FileClose(Dqn_OSFile *file)
{
2024-02-11 07:23:13 +00:00
if (!file || !file->handle || file->error)
return;
fclose(DQN_CAST(FILE *)file->handle);
*file = {};
}
#endif // !defined(DQN_NO_OS_FILE_API)
// NOTE: [$EXEC] Dqn_OSExec ////////////////////////////////////////////////////////////////////////
DQN_API void Dqn_OS_Exit(int32_t exit_code)
{
exit(DQN_CAST(int)exit_code);
}
DQN_API Dqn_OSExecResult Dqn_OS_ExecWait(Dqn_OSExecAsyncHandle handle)
{
Dqn_OSExecResult result = {};
if (!handle.process || handle.os_error_code) {
result.os_error_code = handle.os_error_code;
return result;
}
if (handle.exit_code) {
result.exit_code = handle.exit_code;
return result;
}
#if defined(DQN_PLATFORM_EMSCRIPTEN)
DQN_ASSERTF(false, "Unsupported operation");
#else
static_assert(sizeof(pid_t) <= sizeof(handle.process), "We store the PID opaquely in a register sized pointer");
pid_t process = {};
DQN_MEMCPY(&process, &handle.process, sizeof(process));
for (;;) {
int status = 0;
if (waitpid(process, &status, 0) < 0) {
result.os_error_code = errno;
break;
}
if (WIFEXITED(status)) {
result.exit_code = WEXITSTATUS(status);
break;
}
if (WIFSIGNALED(status)) {
result.os_error_code = WTERMSIG(status);
break;
}
}
#endif
return result;
}
DQN_API Dqn_OSExecAsyncHandle Dqn_OS_ExecAsync(Dqn_Slice<Dqn_Str8> cmd_line, Dqn_Str8 working_dir)
{
Dqn_OSExecAsyncHandle result = {};
if (cmd_line.size == 0)
return result;
Dqn_Scratch scratch = Dqn_Scratch_Get(nullptr);
// TODO: This API will need to switch to an array of strings for unix
pid_t child_pid = fork();
if (child_pid < 0) {
result.os_error_code = errno;
return result;
}
if (child_pid == 0) {
// NOTE: Convert the command into something suitable for execvp
char **argv = Dqn_Arena_NewArray(scratch.arena, char*, cmd_line.size + 1 /*null*/, Dqn_ZeroMem_Yes);
if (!argv) {
result.exit_code = -1;
return result;
}
for (Dqn_usize arg_index = 0; arg_index < cmd_line.size; arg_index++) {
Dqn_Str8 arg = cmd_line.data[arg_index];
argv[arg_index] = Dqn_Str8_Copy(scratch.arena, arg).data; // NOTE: Copy string to guarantee it is null-terminated
}
// NOTE: Change the working directory if there is one
char *prev_working_dir = nullptr;
DQN_DEFER {
if (!prev_working_dir)
return;
if (result.os_error_code == 0)
chdir(prev_working_dir);
free(prev_working_dir);
};
if (working_dir.size) {
prev_working_dir = get_current_dir_name();
if (chdir(working_dir.data) == -1) {
result.os_error_code = errno;
return result;
}
}
// NOTE: Execute the command. We reuse argv because the first arg, the
// binary to execute is guaranteed to be null-terminated.
if (execvp(argv[0], argv) < 0) {
result.os_error_code = errno;
return result;
}
}
DQN_MEMCPY(&result.process, &child_pid, sizeof(child_pid));
return result;
}
#if !defined(DQN_NO_SEMAPHORE)
// NOTE: [$SEMA] Dqn_OSSemaphore ///////////////////////////////////////////////////////////////////
DQN_API Dqn_OSSemaphore Dqn_OS_SemaphoreInit(uint32_t initial_count)
{
Dqn_OSSemaphore result = {};
int pshared = 0; // Share the semaphore across all threads in the process
if (sem_init(&result.posix_handle, pshared, initial_count) == 0)
result.posix_init = true;
return result;
}
DQN_API bool Dqn_OS_SemaphoreIsValid(Dqn_OSSemaphore *semaphore)
{
bool result = false;
if (semaphore) {
result = semaphore->posix_init;
}
return result;
}
DQN_API void Dqn_OS_SemaphoreDeinit(Dqn_OSSemaphore *semaphore)
{
if (!Dqn_OS_SemaphoreIsValid(semaphore))
return;
// TODO(doyle): Error handling?
if (semaphore->posix_init)
sem_destroy(&semaphore->posix_handle);
*semaphore = {};
}
// NOTE: These functions don't need semaphore to be passed by pointer, **BUT**
// the POSIX implementation disallows copies of sem_t. In particular:
//
// Source: The Open Group Base Specifications Issue 7, 2018 edition
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09_09
//
// 2.9.9 Synchronization Object Copies and Alternative Mappings
//
// For barriers, condition variables, mutexes, and read-write locks, [TSH]
// [Option Start] if the process-shared attribute is set to
// PTHREAD_PROCESS_PRIVATE, [Option End] only the synchronization object at the
// address used to initialize it can be used for performing synchronization. The
// effect of referring to another mapping of the same object when locking,
// unlocking, or destroying the object is undefined. [...] The effect of
// referring to a copy of the object when locking, unlocking, or destroying it
// is undefined.
DQN_API void Dqn_OS_SemaphoreIncrement(Dqn_OSSemaphore *semaphore, uint32_t amount)
{
if (!Dqn_OS_SemaphoreIsValid(semaphore))
return;
#if defined(DQN_OS_WIN32)
sem_post_multiple(&semaphore->posix_handle, amount); // mingw extension
#else
DQN_FOR_UINDEX(index, amount)
sem_post(&semaphore->posix_handle);
#endif // !defined(DQN_OS_WIN32)
}
DQN_API Dqn_OSSemaphoreWaitResult Dqn_OS_SemaphoreWait(Dqn_OSSemaphore *semaphore, uint32_t timeout_ms)
{
Dqn_OSSemaphoreWaitResult result = {};
if (!Dqn_OS_SemaphoreIsValid(semaphore))
return result;
if (timeout_ms == DQN_OS_SEMAPHORE_INFINITE_TIMEOUT) {
int wait_result = 0;
do {
wait_result = sem_wait(&semaphore->posix_handle);
} while (wait_result == -1 && errno == EINTR);
if (wait_result == 0)
result = Dqn_OSSemaphoreWaitResult_Success;
} else {
struct timespec abs_timeout = {};
abs_timeout.tv_sec = timeout_ms / 1000;
abs_timeout.tv_nsec = (timeout_ms % 1000) * 1'000'000;
if (sem_timedwait(&semaphore->posix_handle, &abs_timeout) == 0) {
result = Dqn_OSSemaphoreWaitResult_Success;
} else {
if (errno == ETIMEDOUT)
result = Dqn_OSSemaphoreWaitResult_Timeout;
}
}
return result;
}
#endif // !defined(DQN_NO_SEMAPHORE)
#if !defined(DQN_NO_THREAD)
// NOTE: [$MUTX] Dqn_OSMutex ///////////////////////////////////////////////////////////////////////
DQN_API Dqn_OSMutex Dqn_OS_MutexInit()
{
Dqn_OSMutex result = {};
if (pthread_mutexattr_init(&result.posix_attribs) != 0)
return result;
if (pthread_mutex_init(&result.posix_handle, &result.posix_attribs) != 0)
return result;
return result;
}
DQN_API void Dqn_OS_MutexDeinit(Dqn_OSMutex *mutex)
{
if (!mutex)
return;
pthread_mutexattr_destroy(&mutex->posix_attribs);
pthread_mutex_destroy(&mutex->posix_handle);
}
DQN_API void Dqn_OS_MutexLock(Dqn_OSMutex *mutex)
{
if (!mutex)
return;
pthread_mutex_lock(&mutex->posix_handle);
}
DQN_API void Dqn_OS_MutexUnlock(Dqn_OSMutex *mutex)
{
if (!mutex)
return;
pthread_mutex_unlock(&mutex->posix_handle);
}
// NOTE: [$THRD] Dqn_OSThread /////////////////////////////////////////////////////////////////////
static void *Dqn_OS_ThreadFunc_(void *user_context)
{
Dqn_OSThread *thread = DQN_CAST(Dqn_OSThread *)user_context;
Dqn_OS_SemaphoreWait(&thread->init_semaphore, DQN_OS_SEMAPHORE_INFINITE_TIMEOUT);
thread->func(thread);
return nullptr;
}
DQN_API bool Dqn_OS_ThreadInit(Dqn_OSThread *thread, Dqn_OSThreadFunc *func, void *user_context)
{
bool result = false;
if (!thread)
return result;
thread->func = func;
thread->user_context = user_context;
thread->init_semaphore = Dqn_OS_SemaphoreInit(0 /*initial_count*/);
// TODO(doyle): Check if semaphore is valid
// NOTE: pthread_t is essentially the thread ID. In Windows, the handle and
// the ID are different things. For pthreads then we just duplicate the
// thread ID to both variables
pthread_t p_thread = {};
static_assert(sizeof(p_thread) <= sizeof(thread->handle),
"We store the thread handle opaquely in our abstraction, "
"there must be enough bytes to store pthread's structure");
static_assert(sizeof(p_thread) <= sizeof(thread->thread_id),
"We store the thread handle opaquely in our abstraction, "
"there must be enough bytes to store pthread's structure");
pthread_attr_t attribs = {};
pthread_attr_init(&attribs);
result = pthread_create(&p_thread, &attribs, Dqn_OS_ThreadFunc_, thread) == 0;
pthread_attr_destroy(&attribs);
if (result) {
DQN_MEMCPY(&thread->handle, &p_thread, sizeof(p_thread));
DQN_MEMCPY(&thread->thread_id, &p_thread, sizeof(p_thread));
}
if (result) {
Dqn_OS_SemaphoreIncrement(&thread->init_semaphore, 1);
} else {
Dqn_OS_SemaphoreDeinit(&thread->init_semaphore);
*thread = {};
}
return result;
}
DQN_API void Dqn_OS_ThreadDeinit(Dqn_OSThread *thread)
{
if (!thread || !thread->handle)
return;
pthread_t thread_id = {};
DQN_MEMCPY(&thread_id, &thread->thread_id, sizeof(thread_id));
void *return_val = nullptr;
pthread_join(thread_id, &return_val);
thread->handle = {};
thread->thread_id = {};
}
DQN_API uint32_t Dqn_OS_ThreadID()
{
pid_t result = gettid();
DQN_ASSERT(gettid() >= 0);
return DQN_CAST(uint32_t)result;
}
#endif // !defined(DQN_NO_THREAD)
// NOTE: [$HTTP] Dqn_OSHttp ////////////////////////////////////////////////////////////////////////
#if 0 // TODO(doyle): Implement websockets for Windows and Emscripten
static EM_BOOL EMWebSocketOnOpenCallback(int type, const EmscriptenWebSocketOpenEvent *event, void *user_context)
{
(void)user_context;
(void)type;
(void)event;
// EMSCRIPTEN_RESULT result = emscripten_websocket_send_utf8_text(event->socket, R"({"jsonrpc":"2.0","id":1,"method": "eth_subscribe","params":["newHeads"]})");
// if (result)
// Dqn_Log_InfoF("Failed to emscripten_websocket_send_utf8_text(): %d\n", result);
return EM_TRUE;
}
static EM_BOOL EMWebSocketOnMsgCallback(int type, const EmscriptenWebSocketMessageEvent *event __attribute__((nonnull)), void *user_context)
{
(void)type;
(void)user_context;
(void)event;
if (event->isText) {
Dqn_Log_InfoF("Received: %.*s", event->numBytes, event->data);
} else {
Dqn_Log_InfoF("Received: %d bytes", event->numBytes);
}
return EM_TRUE;
}
static EM_BOOL EMWebSocketOnErrorCallback(int type, const EmscriptenWebSocketErrorEvent *event, void *user_context)
{
(void)user_context;
(void)type;
(void)event;
return EM_TRUE;
}
static EM_BOOL EMWebSocketOnCloseCallback(int type, const EmscriptenWebSocketCloseEvent *event, void *user_context)
{
(void)user_context;
(void)type;
(void)event;
return EM_TRUE;
}
#endif
#if defined(DQN_PLATFORM_EMSCRIPTEN)
static void Dqn_OS_HttpRequestEMFetchOnSuccessCallback(emscripten_fetch_t *fetch)
{
Dqn_OSHttpResponse *response = DQN_CAST(Dqn_OSHttpResponse *)fetch->userData;
if (!DQN_CHECK(response))
return;
response->http_status = DQN_CAST(uint32_t)fetch->status;
response->body = Dqn_Str8_Alloc(response->arena, fetch->numBytes, Dqn_ZeroMem_No);
if (response->body.data)
DQN_MEMCPY(response->body.data, fetch->data, fetch->numBytes);
Dqn_OS_SemaphoreIncrement(&response->on_complete_semaphore, 1);
Dqn_Atomic_AddU32(&response->done, 1);
}
static void Dqn_OS_HttpRequestEMFetchOnErrorCallback(emscripten_fetch_t *fetch)
{
Dqn_OSHttpResponse *response = DQN_CAST(Dqn_OSHttpResponse *)fetch->userData;
if (!DQN_CHECK(response))
return;
response->http_status = DQN_CAST(uint32_t)fetch->status;
response->body = Dqn_Str8_Alloc(response->arena, fetch->numBytes, Dqn_ZeroMem_No);
if (response->body.size)
DQN_MEMCPY(response->body.data, fetch->data, fetch->numBytes);
Dqn_OS_SemaphoreIncrement(&response->on_complete_semaphore, 1);
Dqn_Atomic_AddU32(&response->done, 1);
}
#endif
DQN_API void Dqn_OS_HttpRequestAsync(Dqn_OSHttpResponse *response,
Dqn_Arena *arena,
Dqn_Str8 host,
Dqn_Str8 path,
Dqn_OSHttpRequestSecure secure,
Dqn_Str8 method,
Dqn_Str8 body,
Dqn_Str8 headers)
{
if (!response || !arena)
return;
response->arena = arena;
response->builder.arena = response->scratch_arena ? response->scratch_arena : &response->tmp_arena;
Dqn_Arena *scratch_arena = response->scratch_arena;
Dqn_Scratch scratch_ = Dqn_Scratch_Get(arena);
if (!scratch_arena)
scratch_arena = scratch_.arena;
#if defined(DQN_PLATFORM_EMSCRIPTEN)
emscripten_fetch_attr_t fetch_attribs = {};
emscripten_fetch_attr_init(&fetch_attribs);
if (method.size >= sizeof(fetch_attribs.requestMethod)) {
response->error_msg = Dqn_Str8_InitF(arena,
"Request method in EM has a size limit of 31 characters, method was '%.*s' which is %zu characters long",
DQN_STR_FMT(method),
method.size);
DQN_CHECKF(method.size < sizeof(fetch_attribs.requestMethod), "%.*s", DQN_STR_FMT(response->error_msg));
response->error_code = DQN_CAST(uint32_t)-1;
Dqn_Atomic_AddU32(&response->done, 1);
return;
}
DQN_MEMCPY(fetch_attribs.requestMethod, method.data, method.size);
fetch_attribs.requestData = body.data;
fetch_attribs.requestDataSize = body.size;
fetch_attribs.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
fetch_attribs.onsuccess = Dqn_OS_HttpRequestEMFetchOnSuccessCallback;
fetch_attribs.onerror = Dqn_OS_HttpRequestEMFetchOnErrorCallback;
fetch_attribs.userData = response;
Dqn_Str8 url = Dqn_Str8_InitF(scratch_arena, "%.*s%.*s", DQN_STR_FMT(host), DQN_STR_FMT(path));
Dqn_Log_InfoF("Initiating HTTP '%s' request to '%.*s' with payload '%.*s'", fetch_attribs.requestMethod, DQN_STR_FMT(url), DQN_STR_FMT(body));
response->on_complete_semaphore = Dqn_OS_SemaphoreInit(0);
response->em_handle = emscripten_fetch(&fetch_attribs, url.data);
#else // #elif defined(DQN_OS_WIN32)
DQN_INVALID_CODE_PATHF("Unimplemented function");
#endif
}
DQN_API void Dqn_OS_HttpRequestFree(Dqn_OSHttpResponse *response)
{
// NOTE: Cleanup
#if defined(DQN_PLATFORM_EMSCRIPTEN)
if (response->em_handle) {
emscripten_fetch_close(response->em_handle);
response->em_handle = nullptr;
}
#endif // #elif defined(DQN_OS_WIN32)
Dqn_Arena_Deinit(&response->tmp_arena);
if (Dqn_OS_SemaphoreIsValid(&response->on_complete_semaphore))
Dqn_OS_SemaphoreDeinit(&response->on_complete_semaphore);
*response = {};
}