From 865567beca599aaffae1e9b262a093a0a0e27446 Mon Sep 17 00:00:00 2001 From: LeonardoBizzoni Date: Thu, 21 Aug 2025 12:14:08 +0200 Subject: [PATCH] logging + [LNX] child process status code --- cbuild.h | 535 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 326 insertions(+), 209 deletions(-) diff --git a/cbuild.h b/cbuild.h index 0126e7a..9381229 100755 --- a/cbuild.h +++ b/cbuild.h @@ -79,34 +79,26 @@ typedef enum {false, true} bool; # undef stdout # define win32_stderr stderr # undef stderr -# define cb_setenv(Varname, Value) SetEnvironmentVariableA((Varname), (Value)) # define _cb_platform_mkdir _mkdir(path); -# define CB_PROC_HANDLE_INVALID INVALID_HANDLE_VALUE -# define CB_FD_INVALID INVALID_HANDLE_VALUE +# define CB_PROC_INVALID INVALID_HANDLE_VALUE +# define CB_HANDLE_INVALID INVALID_HANDLE_VALUE - typedef HANDLE cb_fd; - typedef HANDLE cb_proc_handle; - - static inline char* cb_getenv(char *varname) { - static char res[32767] = {}; - GetEnvironmentVariableA(varname, res, sizeof(res)); - return res; - } + typedef HANDLE CB_Handle; + typedef HANDLE CB_ProcHandle; #else # include # include # include # include # include -# define cb_getenv(Varname) getenv((Varname)) -# define cb_setenv(Varname, Value) setenv((Varname), (Value), true) # define _cb_platform_mkdir mkdir(path, S_IRWXU | (S_IRGRP | S_IXGRP) | (S_IROTH | S_IXOTH)); -# define CB_PROC_HANDLE_INVALID -1 -# define CB_FD_INVALID -1 - typedef int32_t cb_fd; - typedef pid_t cb_proc_handle; +# define CB_PROC_INVALID -1 +# define CB_HANDLE_INVALID -1 + typedef int32_t CB_Handle; + typedef pid_t CB_ProcHandle; #endif +#define internal static #ifndef _assert_break # if OS_WINDOWS # define _assert_break() __debugbreak() @@ -140,22 +132,34 @@ struct cb_run_args { bool async; bool reset; - cb_fd stdin; - cb_fd stdout; - cb_fd stderr; + CB_Handle stdin; + CB_Handle stdout; + CB_Handle stderr; }; typedef struct { - cb_proc_handle *values; + int32_t status_code; + CB_ProcHandle handle; +} CB_Process; + +typedef struct { + CB_Process *values; size_t count; size_t capacity; -} cb_procs; +} CB_ProcessList; + +typedef uint8_t CB_AccessFlag; +enum { + CB_AccessFlag_Read = 1 << 0, + CB_AccessFlag_Write = 1 << 1, + CB_AccessFlag_Append = 1 << 2, +}; -typedef uint8_t cb_acf; +typedef uint8_t CB_LogLevel; enum { - CB_ACF_READ = 1 << 0, - CB_ACF_WRITE = 1 << 1, - CB_ACF_APPEND = 1 << 2, + CB_LogLevel_Info, + CB_LogLevel_Warn, + CB_LogLevel_Error, }; #ifndef CB_DYN_DEFAULT_CAPACITY @@ -184,9 +188,7 @@ enum { .reset = true, \ __VA_ARGS__ \ }) -#define cb_procs_push(Dynarr, Value) cb_dyn_push(Dynarr, Value) -static void cb_procs_wait(cb_procs *procs); -static void cb_proc_wait(cb_proc_handle handle); +#define cb_processesslist_push(Dynarr, Value) cb_dyn_push(Dynarr, Value) #define cb_dyn_free_custom(Dynarr, Values, Count) \ do { \ @@ -219,188 +221,126 @@ static void cb_proc_wait(cb_proc_handle handle); (Dynarr)->Count += (Size); \ } while(0) -static bool _cb_need_rebuild(char *output_path, struct cb_path_list sources); -static void _cb_rebuild(int argc, char **argv, char *cb_src, ...); -static cb_proc_handle _cb_run(cb_cmd *cmd, struct cb_run_args args); - -static void _cb_rebuild(int argc, char **argv, char *builder_src, ...) { - Assert(argc >= 1); - char *exe_name = argv[0]; - - struct cb_path_list sources = {}; - cb_dyn_push(&sources, builder_src); - +static char* cb_format(const char *format, ...); +static void cb_print(CB_LogLevel level, const char *fmt, ...); +static char* cb_getenv(char *varname); +static int32_t cb_setenv(char *varname, char *value); +static void cb_processesslist_wait(CB_ProcessList *procs); +static void cb_process_wait(CB_Process *handle); + +internal bool _cb_need_rebuild(char *output_path, struct cb_path_list sources); +internal void _cb_rebuild(int argc, char **argv, char *cb_src, ...); +internal CB_Process _cb_run(cb_cmd *cmd, struct cb_run_args args); +internal size_t _last_occurance_of(char *string, char ch); + +// ============================================================================== +// Implementation +static char* cb_format(const char *format, ...) { va_list args; - va_start(args, builder_src); - for (;;) { - char *path = va_arg(args, char*); - if (!path) { break; } - cb_dyn_push(&sources, path); - } + va_start(args, format); + uint32_t needed_bytes = vsnprintf(0, 0, format, args) + 1; va_end(args); - if (!_cb_need_rebuild(exe_name, sources)) { - cb_dyn_free(&sources); - return; - } + char *res = malloc(needed_bytes); + va_start(args, format); + (void)vsnprintf(res, needed_bytes, format, args); + res[needed_bytes] = 0; + va_end(args); + return res; +} -#if OS_WINDOWS - char *old = ".old"; - char *exe_name_old = malloc(strlen(exe_name) + strlen(old) + 1); - memcpy(exe_name_old, exe_name, strlen(exe_name)); - memcpy(exe_name_old + strlen(exe_name), old, strlen(old) + 1); - printf("\trenaming: %s -> %s\n", exe_name, exe_name_old); - if (!MoveFileEx(exe_name, exe_name_old, - MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - printf("\tMoveFileEx error: %d\n", GetLastError()); - exit(-1); +static void cb_print(CB_LogLevel level, const char *fmt, ...) { +#define ANSI_COLOR_RED "\x1b[31m" +#define ANSI_COLOR_GREEN "\x1b[32m" +#define ANSI_COLOR_YELLOW "\x1b[33m" +#define ANSI_COLOR_BLUE "\x1b[34m" +#define ANSI_COLOR_MAGENTA "\x1b[35m" +#define ANSI_COLOR_CYAN "\x1b[36m" +#define ANSI_COLOR_RESET "\x1b[0m" + va_list args; + switch (level) { + case CB_LogLevel_Info: { + printf(ANSI_COLOR_CYAN "[INFO] "); + } break; + case CB_LogLevel_Warn: { + printf(ANSI_COLOR_YELLOW "[WARNING] "); + } break; + case CB_LogLevel_Error: { + printf(ANSI_COLOR_RED "[ERROR] "); + } break; + default: printf(ANSI_COLOR_RESET); goto print_str; } -#endif - - cb_cmd cmd = {}; - cb_cmd_append(&cmd, CB_CMD_REBUILD_SELF(exe_name, builder_src)); - cb_run(&cmd); + printf(ANSI_COLOR_RESET); - cb_cmd_push(&cmd, exe_name); - cb_cmd_append_dyn(&cmd, argv, argc); - (void)cb_run(&cmd); - exit(0); + print_str: ; + va_start(args, fmt); + printf("%s\n", cb_format(fmt, args)); + va_end(args); +#undef ANSI_COLOR_RED +#undef ANSI_COLOR_GREEN +#undef ANSI_COLOR_YELLOW +#undef ANSI_COLOR_BLUE +#undef ANSI_COLOR_MAGENTA +#undef ANSI_COLOR_CYAN +#undef ANSI_COLOR_RESET } -static bool _cb_need_rebuild(char *output_path, struct cb_path_list sources) { +static char* cb_getenv(char *varname) { #if OS_WINDOWS - FILETIME output_mtime_large = {}; - HANDLE output_handle = CreateFileA(output_path, GENERIC_READ, FILE_SHARE_READ, - 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); - if (output_handle == INVALID_HANDLE_VALUE || - !GetFileTime(output_handle, 0, 0, &output_mtime_large)) { - CloseHandle(output_handle); - return true; - } - CloseHandle(output_handle); - - ULARGE_INTEGER output_mtime = {}; - output_mtime.LowPart = output_mtime_large.dwLowDateTime; - output_mtime.HighPart = output_mtime_large.dwHighDateTime; - - for (size_t i = 0; i < sources.count; ++i) { - FILETIME source_mtime_large = {}; - HANDLE source_handle = CreateFileA(sources.values[i], GENERIC_READ, FILE_SHARE_READ, - 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); - if (source_handle == INVALID_HANDLE_VALUE) { return true; } - if (!GetFileTime(source_handle, 0, 0, &source_mtime_large)) { - CloseHandle(output_handle); - return true; - } - CloseHandle(output_handle); - - ULARGE_INTEGER source_mtime = {}; - source_mtime.LowPart = source_mtime_large.dwLowDateTime; - source_mtime.HighPart = source_mtime_large.dwHighDateTime; - if (output_mtime.QuadPart < source_mtime.QuadPart) { - return true; - } + char *res = malloc((sizeof *res) * 32767); + if (!GetEnvironmentVariableA(varname, res, (sizeof *res) * 32767)) { + free(res); + res = 0; } - return false; + return res; #else - // NOTE(lb): on `fstat` failure assume needed rebuild - struct stat output_stat = {}; - if (stat(output_path, &output_stat) < 0) { return true; } - for (size_t i = 0; i < sources.count; ++i) { - struct stat source_stat = {}; - if (stat(sources.values[i], &source_stat) < 0 || - output_stat.st_mtime < source_stat.st_mtime) { return true; } - } - return false; + return getenv(varname); #endif } -static cb_proc_handle _cb_run(cb_cmd *cmd, struct cb_run_args args) { - cb_proc_handle res = {}; - +static int32_t cb_setenv(char *varname, char *value) { #if OS_WINDOWS - char cmdline[MAX_PATH] = {}; - size_t offset = 0; - for (size_t i = 0; i < cmd->count; ++i) { - if (strstr(cmd->values[i], " ") || strstr(cmd->values[i], "\\")) { - strcat(cmdline, "\""); - strcat(cmdline, cmd->values[i]); - strcat(cmdline, "\""); - } else { - strcat(cmdline, cmd->values[i]); - } - if (i != cmd->count - 1) { strcat(cmdline, " "); } - } - - STARTUPINFO si = {0}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; - si.hStdInput = args.stdin ? args.stdin : GetStdHandle(STD_INPUT_HANDLE); - si.hStdOutput = args.stdout ? args.stdout : GetStdHandle(STD_OUTPUT_HANDLE); - si.hStdError = args.stderr ? args.stderr : GetStdHandle(STD_ERROR_HANDLE); - - PROCESS_INFORMATION pi = {}; - if (!CreateProcessA(0, cmdline, 0, 0, TRUE, 0, 0, 0, &si, &pi)) { - printf("CreateProcess error: %d\n", GetLastError()); - exit(-1); - } - CloseHandle(pi.hThread); - res = pi.hProcess; + return SetEnvironmentVariableA(varname, value); #else - res = fork(); - if (!res) { - if (args.stdout) { dup2(args.stdout, STDOUT_FILENO); } - if (args.stderr) { dup2(args.stderr, STDERR_FILENO); } - if (args.stdin) { - lseek(args.stdin, 0, SEEK_SET); - dup2(args.stdin, STDIN_FILENO); - } - - cb_cmd _cmd = {}; - cb_cmd_append_dyn(&_cmd, cmd->values, cmd->count); - cb_cmd_push(&_cmd, 0); - if (execvp(_cmd.values[0], _cmd.values) < 0) { - fprintf(stderr, "couldn't start child process `%s`: %s\n", - _cmd.values[0], strerror(errno)); - exit(-1); - } - // NOTE(lb): unreachable, execvp only returns on error. - } + return !setenv(varname, value, true); #endif +} - if (args.reset) { - cmd->count = 0; - } - if (!args.async) { - cb_proc_wait(res); - res = CB_PROC_HANDLE_INVALID; +static void cb_process_wait(CB_Process *proc) { + if (proc->handle == CB_PROC_INVALID) { + cb_print(CB_LogLevel_Warn, "Waiting on invalid process handle"); + return; } - return res; -} -static void cb_proc_wait(cb_proc_handle handle) { - if (handle == CB_PROC_HANDLE_INVALID) { return; } #if OS_WINDOWS - WaitForSingleObject(handle, INFINITE); - CloseHandle(handle); + WaitForSingleObject(proc->handle, INFINITE); + CloseHandle(proc->handle); #else - Assert(waitpid(handle, 0, 0) == handle); + int32_t status = 0; + Assert(waitpid(proc->handle, &status, 0) == proc->handle); + if (WIFEXITED(status)) { + proc->status_code = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + proc->status_code = WTERMSIG(status); + } else { + proc->status_code = 0; + } #endif } -static void cb_procs_wait(cb_procs *procs) { +static void cb_processesslist_wait(CB_ProcessList *procs) { for (size_t i = 0; i < procs->count; ++i) { - cb_proc_wait(procs->values[i]); + cb_process_wait(&procs->values[i]); } cb_dyn_free(procs); } -static cb_fd cb_open(char *path, cb_acf permission) { +static CB_Handle cb_handle_open(char *path, CB_AccessFlag permission) { #if OS_WINDOWS DWORD access_flags = 0; - if(permission & CB_ACF_READ) { access_flags |= GENERIC_READ; } - if(permission & CB_ACF_WRITE) { access_flags |= GENERIC_WRITE; } - if(permission & CB_ACF_APPEND) { access_flags |= FILE_APPEND_DATA; } + if(permission & CB_AccessFlag_Read) { access_flags |= GENERIC_READ; } + if(permission & CB_AccessFlag_Write) { access_flags |= GENERIC_WRITE; } + if(permission & CB_AccessFlag_Append) { access_flags |= FILE_APPEND_DATA; } SECURITY_ATTRIBUTES sa = {0}; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.bInheritHandle = TRUE; @@ -408,19 +348,39 @@ static cb_fd cb_open(char *path, cb_acf permission) { OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); #else int32_t flags = O_CREAT; - if (permission & CB_ACF_APPEND) { flags |= O_APPEND | O_CREAT; } - if ((permission & CB_ACF_READ) && (permission & CB_ACF_WRITE)) { + if (permission & CB_AccessFlag_Append) { flags |= O_APPEND | O_CREAT; } + if ((permission & CB_AccessFlag_Read) && (permission & CB_AccessFlag_Write)) { flags |= O_RDWR; - } else if (permission & CB_ACF_READ) { + } else if (permission & CB_AccessFlag_Read) { flags |= O_RDONLY; - } else if (permission & CB_ACF_WRITE) { + } else if (permission & CB_AccessFlag_Write) { flags |= O_WRONLY | O_CREAT | O_TRUNC; } return open(path, flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); #endif } -static char* cb_read(cb_fd fd) { +static void cb_handle_close(CB_Handle fd) { + if (fd == CB_HANDLE_INVALID) { + cb_print(CB_LogLevel_Warn, "Closing invalid handle"); + return; + } + +#if OS_WINDOWS + FlushFileBuffers(fd); + CloseHandle(fd); +#else + fsync(fd); + close(fd); +#endif +} + +static char* cb_handle_read(CB_Handle fd) { + if (fd == CB_HANDLE_INVALID) { + cb_print(CB_LogLevel_Warn, "Reading from invalid handle"); + return 0; + } + #if OS_WINDOWS #else struct stat file_stat; @@ -434,32 +394,19 @@ static char* cb_read(cb_fd fd) { #endif } -static void cb_write(cb_fd fd, char *buffer, size_t buffsize) { -#if OS_WINDOWS -#else - write(fd, buffer, buffsize); -#endif -} +static void cb_handle_write(CB_Handle fd, char *buffer, size_t buffsize) { + if (fd == CB_HANDLE_INVALID) { + cb_print(CB_LogLevel_Warn, "Writing to invalid handle"); + return; + } -static void cb_close(cb_fd fd) { #if OS_WINDOWS - if (fd != CB_FD_INVALID) { FlushFileBuffers(fd); } - CloseHandle(fd); #else - if (fd != CB_FD_INVALID) { fsync(fd); } - close(fd); + write(fd, buffer, buffsize); #endif } -static size_t _last_occurance_of(char *string, char ch) { - char *res = string; - for (char *curr = string; curr && *curr; ++curr) { - if (*curr == ch) { res = curr; } - } - return res - string; -} - -static bool cb_mkdir(char *path) { +static bool cb_dir_create(char *path) { int32_t mkdir_res = _cb_platform_mkdir(path); if (mkdir_res < 0 && errno == ENOENT) { size_t parent_end = _last_occurance_of(path, '/'); @@ -467,7 +414,7 @@ static bool cb_mkdir(char *path) { char *parent = malloc(parent_end + 1); memcpy(parent, path, parent_end); parent[parent_end] = 0; - cb_mkdir(parent); + cb_dir_create(parent); free(parent); _cb_platform_mkdir(path); } @@ -475,25 +422,195 @@ static bool cb_mkdir(char *path) { return !mkdir_res; } -static void cb_rmdir(char *path) { +static void cb_dir_remove(char *path) { #if OS_WINDOWS #else rmdir(path); #endif } -static void cb_fs_delete(char *path) { +static void cb_file_delete(char *path) { #if OS_WINDOWS #else unlink(path); #endif } -static void cb_fs_rename(char *path, char *to) { +static bool cb_file_rename(char *path, char *to) { #if OS_WINDOWS + return MoveFileEx(exe_name, exe_name_old, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) #else - rename(path, to); + return !rename(path, to); #endif } +internal CB_Process _cb_run(cb_cmd *cmd, struct cb_run_args args) { + CB_Process res = {}; + +#if OS_WINDOWS + char cmdline[MAX_PATH] = {}; + size_t offset = 0; + for (size_t i = 0; i < cmd->count; ++i) { + strcat(cmdline, strstr(cmd->values[i], " ") || strstr(cmd->values[i], "\\") + ? cb_format("\"%s\"", cmd->values[i]) + : cmd->values[i]); + if (i != cmd->count - 1) { strcat(cmdline, " "); } + } + + STARTUPINFO si = {0}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = args.stdin ? args.stdin : GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = args.stdout ? args.stdout : GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = args.stderr ? args.stderr : GetStdHandle(STD_ERROR_HANDLE); + + PROCESS_INFORMATION pi = {}; + if (!CreateProcessA(0, cmdline, 0, 0, TRUE, 0, 0, 0, &si, &pi)) { + char *msg = 0; + DWORD error = GetLastError(); + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + 0, error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + &msg, 0, 0); + cb_print(CB_LogLevel_Error, "Child process creation failed with error %d: %s\n", + error, msg); + exit(-1); + } + CloseHandle(pi.hThread); + res = pi.hProcess; +#else + res.handle = fork(); + if (res.handle < 0) { + cb_print(CB_LogLevel_Error, "Child process creation failed with error %d: %s\n", + errno, strerror(errno)); + exit(-1); + } else if (!res.handle) { + if (args.stdout) { dup2(args.stdout, STDOUT_FILENO); } + if (args.stderr) { dup2(args.stderr, STDERR_FILENO); } + if (args.stdin) { + lseek(args.stdin, 0, SEEK_SET); + dup2(args.stdin, STDIN_FILENO); + } + + cb_cmd _cmd = {}; + cb_cmd_append_dyn(&_cmd, cmd->values, cmd->count); + cb_cmd_push(&_cmd, 0); + if (execvp(_cmd.values[0], _cmd.values) < 0) { + cb_print(CB_LogLevel_Error, "Child process creation failed with error %d: %s\n", + errno, strerror(errno)); + exit(-1); + } + // NOTE(lb): unreachable, execvp only returns on error. + } +#endif + + if (args.reset) { + cmd->count = 0; + } + if (!args.async) { + cb_process_wait(&res); + res.handle = CB_PROC_INVALID; + } + return res; +} + +internal void _cb_rebuild(int argc, char **argv, char *builder_src, ...) { + Assert(argc >= 1); + char *exe_name = argv[0]; + + struct cb_path_list sources = {}; + cb_dyn_push(&sources, builder_src); + + va_list args; + va_start(args, builder_src); + for (;;) { + char *path = va_arg(args, char*); + if (!path) { break; } + cb_dyn_push(&sources, path); + } + va_end(args); + + if (!_cb_need_rebuild(exe_name, sources)) { + cb_dyn_free(&sources); + return; + } + +#if OS_WINDOWS + char *exe_name_old = cb_format("%s.old", exe_name); + if (!cb_file_rename(exe_name, exe_name_old)) { + cb_print(CB_LogLevel_Error, "File rename failed: %s -> %s", + exe_name, exe_name_old); + exit(-1); + } +#endif + + cb_cmd cmd = {}; + cb_cmd_append(&cmd, CB_CMD_REBUILD_SELF(exe_name, builder_src)); + CB_Process recompiler = cb_run(&cmd); + if (recompiler.status_code) { exit(-1); } + + cb_cmd_push(&cmd, exe_name); + cb_cmd_append_dyn(&cmd, argv, argc); + (void)cb_run(&cmd); + exit(0); +} + +internal bool _cb_need_rebuild(char *output_path, struct cb_path_list sources) { +#if OS_WINDOWS + FILETIME output_mtime_large = {}; + HANDLE output_handle = CreateFileA(output_path, GENERIC_READ, FILE_SHARE_READ, + 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + if (output_handle == INVALID_HANDLE_VALUE || + !GetFileTime(output_handle, 0, 0, &output_mtime_large)) { + CloseHandle(output_handle); + return true; + } + CloseHandle(output_handle); + + ULARGE_INTEGER output_mtime = {}; + output_mtime.LowPart = output_mtime_large.dwLowDateTime; + output_mtime.HighPart = output_mtime_large.dwHighDateTime; + + for (size_t i = 0; i < sources.count; ++i) { + FILETIME source_mtime_large = {}; + HANDLE source_handle = CreateFileA(sources.values[i], GENERIC_READ, FILE_SHARE_READ, + 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + if (source_handle == INVALID_HANDLE_VALUE) { return true; } + if (!GetFileTime(source_handle, 0, 0, &source_mtime_large)) { + CloseHandle(output_handle); + return true; + } + CloseHandle(output_handle); + + ULARGE_INTEGER source_mtime = {}; + source_mtime.LowPart = source_mtime_large.dwLowDateTime; + source_mtime.HighPart = source_mtime_large.dwHighDateTime; + if (output_mtime.QuadPart < source_mtime.QuadPart) { + return true; + } + } + return false; +#else + // NOTE(lb): on `fstat` failure assume needed rebuild + struct stat output_stat = {}; + if (stat(output_path, &output_stat) < 0) { return true; } + for (size_t i = 0; i < sources.count; ++i) { + struct stat source_stat = {}; + if (stat(sources.values[i], &source_stat) < 0 || + output_stat.st_mtime < source_stat.st_mtime) { return true; } + } + return false; +#endif +} + +internal size_t _last_occurance_of(char *string, char ch) { + char *res = string; + for (char *curr = string; curr && *curr; ++curr) { + if (*curr == ch) { res = curr; } + } + return res - string; +} + #endif -- 2.52.0