From 8133a8d3cb7c70e211ef8e11fde5e4371b18a9fd Mon Sep 17 00:00:00 2001 From: CPunch Date: Sun, 23 Jan 2022 21:28:16 -0600 Subject: [PATCH] Inital commit lib/ is just [FoxNet](https://git.openpunk.com/CPunch/FoxNet) ported to C99 --- .vscode/settings.json | 37 ++ CMakeLists.txt | 6 + README.md | 7 + bot/CMakeLists.txt | 25 ++ bot/src/main.c | 0 cnc/CMakeLists.txt | 25 ++ cnc/include/cnc.h | 12 + cnc/src/cnc.c | 1 + cnc/src/main.c | 0 lib/CMakeLists.txt | 31 ++ lib/include/hashmap.h | 54 +++ lib/include/laika.h | 13 + lib/include/lerror.h | 47 ++ lib/include/lmem.h | 26 ++ lib/include/lpacket.h | 10 + lib/include/lpeer.h | 25 ++ lib/include/lpolllist.h | 43 ++ lib/include/lsocket.h | 86 ++++ lib/src/hashmap.c | 936 ++++++++++++++++++++++++++++++++++++++++ lib/src/lerror.c | 4 + lib/src/lmem.c | 18 + lib/src/lpeer.c | 88 ++++ lib/src/lpolllist.c | 191 ++++++++ lib/src/lsocket.c | 254 +++++++++++ 24 files changed, 1939 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 bot/CMakeLists.txt create mode 100644 bot/src/main.c create mode 100644 cnc/CMakeLists.txt create mode 100644 cnc/include/cnc.h create mode 100644 cnc/src/cnc.c create mode 100644 cnc/src/main.c create mode 100644 lib/CMakeLists.txt create mode 100644 lib/include/hashmap.h create mode 100644 lib/include/laika.h create mode 100644 lib/include/lerror.h create mode 100644 lib/include/lmem.h create mode 100644 lib/include/lpacket.h create mode 100644 lib/include/lpeer.h create mode 100644 lib/include/lpolllist.h create mode 100644 lib/include/lsocket.h create mode 100644 lib/src/hashmap.c create mode 100644 lib/src/lerror.c create mode 100644 lib/src/lmem.c create mode 100644 lib/src/lpeer.c create mode 100644 lib/src/lpolllist.c create mode 100644 lib/src/lsocket.c diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..18aa667 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,37 @@ +{ + "files.associations": { + "*.tcc": "cpp", + "deque": "cpp", + "list": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "cinttypes": "cpp", + "cstring": "cpp", + "algorithm": "cpp", + "chrono": "cpp", + "array": "cpp", + "compare": "cpp", + "functional": "cpp", + "istream": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "variant": "cpp" + }, + "cSpell.words": [ + "CWARN", + "epollfd", + "EPOLLIN", + "EWOULD", + "ISPROTECTED", + "Laika", + "LAIKAMAGIC", + "LAIKAMAGICLEN", + "NOMINMAX", + "rmvarray" + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..67d9ecc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.10) + +# compile laikalib, cnc & bot +add_subdirectory(lib) +add_subdirectory(cnc) +add_subdirectory(bot) diff --git a/README.md b/README.md new file mode 100644 index 0000000..70758c7 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Laika + +This is a simple POC botnet written in C99. The net library shares some similarities to [FoxNet](https://github.com/CPunch/FoxNet), in fact lpolllist.c is just a port of the FoxPollList class. This botnet is also crossplatform with clients written for each platform (check the /clients folder). + +## Why? + +Malware development in recent years is soooooooo boring (esp. malware written by non-nationstate actors). I wanted to prove that homebrew malware can still be interesting and fun! Obviously use this on your own machines/get permission before running etc. etc. \ No newline at end of file diff --git a/bot/CMakeLists.txt b/bot/CMakeLists.txt new file mode 100644 index 0000000..c69ac43 --- /dev/null +++ b/bot/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(BOT_INCLUDEDIR ${CMAKE_CURRENT_SOURCE_DIR}/include) + +project(LaikaBot VERSION 1.0) + +# Put CMake targets (ALL_BUILD/ZERO_CHECK) into a folder +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +# Set the project as the default startup project for VS +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT LaikaBot) + +# compile LaikaBot +file(GLOB_RECURSE BOTSOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/**.c) +add_executable(LaikaBot ${BOTSOURCE}) +target_link_libraries(LaikaBot PUBLIC LaikaLib) + +# add the 'DEBUG' preprocessor definition if we're compiling as Debug +target_compile_definitions(LaikaBot PUBLIC "$<$:DEBUG>") + +# add include directory +target_include_directories(LaikaBot PUBLIC ${BOT_INCLUDEDIR}) \ No newline at end of file diff --git a/bot/src/main.c b/bot/src/main.c new file mode 100644 index 0000000..e69de29 diff --git a/cnc/CMakeLists.txt b/cnc/CMakeLists.txt new file mode 100644 index 0000000..182ae7e --- /dev/null +++ b/cnc/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(CNC_INCLUDEDIR ${CMAKE_CURRENT_SOURCE_DIR}/include) + +project(LaikaCNC VERSION 1.0) + +# Put CMake targets (ALL_BUILD/ZERO_CHECK) into a folder +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +# Set the project as the default startup project for VS +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT LaikaCNC) + +# compile LaikaCNC +file(GLOB_RECURSE CNCSOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/**.c) +add_executable(LaikaCNC ${CNCSOURCE}) +target_link_libraries(LaikaCNC PUBLIC LaikaLib) + +# add the 'DEBUG' preprocessor definition if we're compiling as Debug +target_compile_definitions(LaikaCNC PUBLIC "$<$:DEBUG>") + +# add include directory +target_include_directories(LaikaCNC PUBLIC ${CNC_INCLUDEDIR}) diff --git a/cnc/include/cnc.h b/cnc/include/cnc.h new file mode 100644 index 0000000..0467f9b --- /dev/null +++ b/cnc/include/cnc.h @@ -0,0 +1,12 @@ +#ifndef LAIKA_CNC_H +#define LAIKA_CNC_H + +#include "laika.h" +#include "lsocket.h" +#include "lpolllist.h" + +struct { + struct sLaika_socket sock; +} sLaika_cnc; + +#endif \ No newline at end of file diff --git a/cnc/src/cnc.c b/cnc/src/cnc.c new file mode 100644 index 0000000..1fcc7d3 --- /dev/null +++ b/cnc/src/cnc.c @@ -0,0 +1 @@ +#include "cnc.h" \ No newline at end of file diff --git a/cnc/src/main.c b/cnc/src/main.c new file mode 100644 index 0000000..e69de29 diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..82c65cb --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.10) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(LIB_INCLUDEDIR ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# version details +set(LIB_VERSION_MAJOR 0) +set(LIB_VERSION_MINOR 0) + +project(LaikaLib VERSION ${LIB_VERSION_MAJOR}.${LIB_VERSION_MINOR}) + +# Put CMake targets (ALL_BUILD/ZERO_CHECK) into a folder +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +# Set the project as the default startup project for VS +set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT LaikaLib) + +# compile LaikaLib library +file(GLOB_RECURSE LIBSOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/**.c) +add_library(LaikaLib STATIC ${LIBSOURCE}) + +# add the version definitions and the 'DEBUG' preprocessor definition if we're compiling as Debug +target_compile_definitions(LaikaLib PUBLIC LIB_VERSION_MAJOR=${LIB_VERSION_MAJOR} LIB_VERSION_MINOR=${LIB_VERSION_MINOR} "$<$:DEBUG>") + +# add include directory +target_include_directories(LaikaLib PUBLIC ${LIB_INCLUDEDIR}) + +# set library name +set_target_properties(LaikaLib PROPERTIES OUTPUT_NAME net-${LIB_VERSION_MAJOR}.${LIB_VERSION_MINOR}) diff --git a/lib/include/hashmap.h b/lib/include/hashmap.h new file mode 100644 index 0000000..d5cb64a --- /dev/null +++ b/lib/include/hashmap.h @@ -0,0 +1,54 @@ +// Copyright 2020 Joshua J Baker. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +#ifndef HASHMAP_H +#define HASHMAP_H + +#include +#include +#include + +struct hashmap; + +struct hashmap *hashmap_new(size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata); +struct hashmap *hashmap_new_with_allocator( + void *(*malloc)(size_t), + void *(*realloc)(void *, size_t), + void (*free)(void*), + size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata); +void hashmap_free(struct hashmap *map); +void hashmap_clear(struct hashmap *map, bool update_cap); +size_t hashmap_count(struct hashmap *map); +bool hashmap_oom(struct hashmap *map); +void *hashmap_get(struct hashmap *map, const void *item); +void *hashmap_set(struct hashmap *map, void *item); +void *hashmap_delete(struct hashmap *map, void *item); +void *hashmap_probe(struct hashmap *map, uint64_t position); +bool hashmap_scan(struct hashmap *map, + bool (*iter)(const void *item, void *udata), void *udata); + +uint64_t hashmap_sip(const void *data, size_t len, + uint64_t seed0, uint64_t seed1); +uint64_t hashmap_murmur(const void *data, size_t len, + uint64_t seed0, uint64_t seed1); + + +// DEPRECATED: use `hashmap_new_with_allocator` +void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void*)); + +#endif diff --git a/lib/include/laika.h b/lib/include/laika.h new file mode 100644 index 0000000..1160049 --- /dev/null +++ b/lib/include/laika.h @@ -0,0 +1,13 @@ +#ifndef LAIKA_LAIKA_H +#define LAIKA_LAIKA_H + +#include +#include +#include +#include +#include +#include + +#define ARRAY_START 4 + +#endif \ No newline at end of file diff --git a/lib/include/lerror.h b/lib/include/lerror.h new file mode 100644 index 0000000..fb554b0 --- /dev/null +++ b/lib/include/lerror.h @@ -0,0 +1,47 @@ +#ifndef LAIKA_ERROR_H +#define LAIKA_ERROR_H + +#include +#include + +/* defines errorstack size */ +#define LAIKA_MAXERRORS 32 + +/* DO NOT RETURN/GOTO/BREAK or otherwise skip LAIKA_TRYEND */ +#define LAIKA_TRY if (setjmp(eLaika_errStack[++eLaika_errIndx]) == 0) { +#define LAIKA_CATCH } else { +#define LAIKA_TRYEND } --eLaika_errIndx; + +/* if eLaika_errIndx is >= 0, we have a safe spot to jump too if an error is thrown */ +#define LAIKA_ISPROTECTED (eLaika_errIndx >= 0) + +/* CERROR(printf args): + if called after a LAIKA_TRY block will jump to the previous LAIKA_CATCH/LAIKA_TRYEND block, + otherwise program is exit()'d. if DEBUG is defined printf is called with passed args, else + arguments are ignored. +*/ +#ifndef DEBUG +#define CERROR(...) { \ + if (LAIKA_ISPROTECTED) \ + longjmp(eLaika_errStack[eLaika_errIndx], 1); \ + else \ + exit(1); \ +} +#define CWARN(...) +#else +#define CERROR(...) { \ + printf("[ERROR] : " __VA_ARGS__); \ + if (LAIKA_ISPROTECTED) \ + longjmp(eLaika_errStack[eLaika_errIndx], 1); \ + else \ + exit(1); \ +} + +#define CWARN(...) \ + printf("[WARN] : " __VA_ARGS__); +#endif + +extern int eLaika_errIndx; +extern jmp_buf eLaika_errStack[LAIKA_MAXERRORS]; + +#endif \ No newline at end of file diff --git a/lib/include/lmem.h b/lib/include/lmem.h new file mode 100644 index 0000000..021bd52 --- /dev/null +++ b/lib/include/lmem.h @@ -0,0 +1,26 @@ +#ifndef LAIKA_MEM_H +#define LAIKA_MEM_H + +#include "laika.h" + +#define GROW_FACTOR 2 + +#define laikaM_malloc(sz) laikaM_realloc(NULL, sz) +#define laikaM_free(buf) laikaM_realloc(buf, 0) +#define laikaM_free(buf) laikaM_realloc(buf, 0) + +#define laikaM_growarray(type, buf, count, capacity) \ + if (count >= capacity || buf == NULL) { \ + capacity *= GROW_FACTOR; \ + buf = (type*)cosmoM_realloc(buf, sizeof(type)*capacity); \ + } + +/* moves array elements above indx down by numElem, removing numElem elements at indx */ +#define laikaM_rmvarray(type, buf, count, indx, numElem) { \ + memmove(&buf[indx], &buf[indx+numElem], ((count-indx)-numElem)*sizeof(type)); \ + count -= numElem; \ +} + +void *laikaM_realloc(void *buf, size_t sz); + +#endif \ No newline at end of file diff --git a/lib/include/lpacket.h b/lib/include/lpacket.h new file mode 100644 index 0000000..366d0cb --- /dev/null +++ b/lib/include/lpacket.h @@ -0,0 +1,10 @@ +#ifndef LAIKA_PACKET_H +#define LAIKA_PACKET_H + +typedef enum { + LAIKAPKT_HANDSHAKE_REQ, + LAIKAPKT_HANDSHAKE_RES, + LAIKAPKT_MAXNONE +} LAIKAPKT_ID; + +#endif \ No newline at end of file diff --git a/lib/include/lpeer.h b/lib/include/lpeer.h new file mode 100644 index 0000000..14c7196 --- /dev/null +++ b/lib/include/lpeer.h @@ -0,0 +1,25 @@ +#ifndef LAIKA_PEER_H +#define LAIKA_PEER_H + +#include "laika.h" +#include "lsocket.h" +#include "lpacket.h" +#include "lpolllist.h" + +struct sLaika_peer { + struct sLaika_socket sock; + struct sLaika_pollList *pList; /* pollList we're active in */ + void (*pktHandler)(struct sLaika_peer *peer, LAIKAPKT_ID id); + size_t pktSize; /* current pkt size */ + LAIKAPKT_ID pktID; /* current pkt ID */ + size_t *pktSizeTable; /* const table to pull pkt size data from */ + bool setPollOut; /* if EPOLLOUT/POLLOUT is set on sock's pollfd */ +}; + +struct sLaika_peer *laikaS_newPeer(void (*pktHandler)(struct sLaika_peer *peer, LAIKAPKT_ID id), struct sLaika_pollList *pList, size_t *pktSizeTable); +void laikaS_freePeer(struct sLaika_peer *peer); + +bool laikaS_handlePeerIn(struct sLaika_peer *peer); +bool laikaS_handlePeerOut(struct sLaika_peer *peer); + +#endif \ No newline at end of file diff --git a/lib/include/lpolllist.h b/lib/include/lpolllist.h new file mode 100644 index 0000000..be7ee48 --- /dev/null +++ b/lib/include/lpolllist.h @@ -0,0 +1,43 @@ +#ifndef LAIKA_POLLLIST_H +#define LAIKA_POLLLIST_H + +#include "laika.h" +#include "lsocket.h" +#include "hashmap.h" + +/* number of pollFDs or epollFDs we expect to start with */ +#define POLLSTARTCAP 8 + +struct sLaika_pollEvent { + struct sLaika_socket *sock; + bool pollIn; + bool pollOut; +}; + +struct sLaika_pollList { + struct hashmap *sockets; + struct sLaika_pollEvent *revents; +#ifdef LAIKA_USE_EPOLL + /* epoll */ + struct epoll_event ev, ep_events[MAX_EPOLL_EVENTS]; + SOCKET epollfd; +#else + /* raw poll descriptor */ + PollFD *fds; + int fdCapacity; + int fdCount; +#endif + int reventCapacity; + int reventCount; +}; + +void laikaP_initPList(struct sLaika_pollList *pList); +void laikaP_cleanPList(struct sLaika_pollList *pList); /* free's all members */ +void laikaP_addSock(struct sLaika_pollList *pList, struct sLaika_socket *sock); +void laikaP_rmvSock(struct sLaika_pollList *pList, struct sLaika_socket *sock); +void laikaP_addPollOut(struct sLaika_pollList *pList, struct sLaika_socket *sock); +void laikaP_rmvPollOut(struct sLaika_pollList *pList, struct sLaika_socket *sock); + +struct sLaika_pollEvent *laikaP_poll(struct sLaika_pollList *pList, int timeout, int *nevents); + +#endif \ No newline at end of file diff --git a/lib/include/lsocket.h b/lib/include/lsocket.h new file mode 100644 index 0000000..64adcf4 --- /dev/null +++ b/lib/include/lsocket.h @@ -0,0 +1,86 @@ +/* socket/winsock headers */ +#ifdef _WIN32 +/* windows */ + #ifndef NOMINMAX + #define NOMINMAX + #endif + #define _WINSOCK_DEPRECATED_NO_WARNINGS + #include + #include + #include + #pragma comment(lib, "Ws2_32.lib") + + typedef char buffer_t; + #define PollFD WSAPOLLFD + #define poll WSAPoll + #define LN_ERRNO WSAGetLastError() + #define LN_EWOULD WSAEWOULDBLOCK + #define LN_MSG_NOSIGNAL 0 + #define SOCKETINVALID(x) (x == INVALID_SOCKET) + #define SOCKETERROR(x) (x == SOCKET_ERROR) +#else +/* posix platform */ + #include + #include + #include + #include + #include + #include +#ifdef __linux__ + #include +/* max events for epoll() */ + #define MAX_EPOLL_EVENTS 128 + #define LAIKA_USE_EPOLL +#endif + #include + #include + + typedef int SOCKET; + typedef void buffer_t; + #define PollFD struct pollfd + #define LN_ERRNO errno + #define LN_EWOULD EWOULDBLOCK + #define LN_MSG_NOSIGNAL MSG_NOSIGNAL + #define INVALID_SOCKET -1 + #define SOCKETINVALID(x) (x < 0) + #define SOCKETERROR(x) (x == -1) +#endif +#include + +typedef enum { + RAWSOCK_OK, + RAWSOCK_ERROR, + RAWSOCK_CLOSED, + RAWSOCK_POLL +} RAWSOCKCODE; + +struct sLaika_socket { + uint8_t *outBuf; /* raw data to be sent() */ + uint8_t *inBuf; /* raw data we recv()'d */ + SOCKET sock; /* raw socket fd */ + int outCount; + int inCount; + int outCap; + int inCap; +}; + +#define laikaS_isAlive(sock) (sock->sock != INVALID_SOCKET) + +void laikaS_init(void); +void laikaS_cleanUp(void); + +void laikaS_initSocket(struct sLaika_socket *sock); +void laikaS_cleanSocket(struct sLaika_socket *sock); +void laikaS_kill(struct sLaika_socket *sock); /* kills a socket */ +void laikaS_connect(struct sLaika_socket *sock, char *ip, char *port); /* connect to ip & port */ +void laikaS_bind(struct sLaika_socket *sock, uint16_t port); /* bind sock to port */ +bool laikaS_setNonBlock(struct sLaika_socket *sock); + +void laikaS_read(struct sLaika_socket *sock, void *buf, size_t sz); /* reads from inBuf */ +void laikaS_write(struct sLaika_socket *sock, void *buf, size_t sz); /* writes to outBuf */ + +void laikaS_writeByte(struct sLaika_socket *sock, uint8_t data); +uint8_t laikaS_readByte(struct sLaika_socket *sock); + +RAWSOCKCODE laikaS_rawRecv(struct sLaika_socket *sock, size_t sz, int *processed); +RAWSOCKCODE laikaS_rawSend(struct sLaika_socket *sock, size_t sz, int *processed); \ No newline at end of file diff --git a/lib/src/hashmap.c b/lib/src/hashmap.c new file mode 100644 index 0000000..e9abcec --- /dev/null +++ b/lib/src/hashmap.c @@ -0,0 +1,936 @@ +// Copyright 2020 Joshua J Baker. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +#include +#include +#include +#include +#include +#include "hashmap.h" + +static void *(*_malloc)(size_t) = NULL; +static void *(*_realloc)(void *, size_t) = NULL; +static void (*_free)(void *) = NULL; + +// hashmap_set_allocator allows for configuring a custom allocator for +// all hashmap library operations. This function, if needed, should be called +// only once at startup and a prior to calling hashmap_new(). +void hashmap_set_allocator(void *(*malloc)(size_t), void (*free)(void*)) +{ + _malloc = malloc; + _free = free; +} + +#define panic(_msg_) { \ + /*fprintf(stderr, "panic: %s (%s:%d)\n", (_msg_), __FILE__, __LINE__);*/ \ + exit(1); \ +} + +struct bucket { + uint64_t hash:48; + uint64_t dib:16; +}; + +// hashmap is an open addressed hash map using robinhood hashing. +struct hashmap { + void *(*malloc)(size_t); + void *(*realloc)(void *, size_t); + void (*free)(void *); + bool oom; + size_t elsize; + size_t cap; + uint64_t seed0; + uint64_t seed1; + uint64_t (*hash)(const void *item, uint64_t seed0, uint64_t seed1); + int (*compare)(const void *a, const void *b, void *udata); + void (*elfree)(void *item); + void *udata; + size_t bucketsz; + size_t nbuckets; + size_t count; + size_t mask; + size_t growat; + size_t shrinkat; + void *buckets; + void *spare; + void *edata; +}; + +static struct bucket *bucket_at(struct hashmap *map, size_t index) { + return (struct bucket*)(((char*)map->buckets)+(map->bucketsz*index)); +} + +static void *bucket_item(struct bucket *entry) { + return ((char*)entry)+sizeof(struct bucket); +} + +static uint64_t get_hash(struct hashmap *map, const void *key) { + return map->hash(key, map->seed0, map->seed1) << 16 >> 16; +} + +// hashmap_new_with_allocator returns a new hash map using a custom allocator. +// See hashmap_new for more information information +struct hashmap *hashmap_new_with_allocator( + void *(*_malloc)(size_t), + void *(*_realloc)(void*, size_t), + void (*_free)(void*), + size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata) +{ + _malloc = _malloc ? _malloc : malloc; + _realloc = _realloc ? _realloc : realloc; + _free = _free ? _free : free; + int ncap = 16; + if (cap < ncap) { + cap = ncap; + } else { + while (ncap < cap) { + ncap *= 2; + } + cap = ncap; + } + size_t bucketsz = sizeof(struct bucket) + elsize; + while (bucketsz & (sizeof(uintptr_t)-1)) { + bucketsz++; + } + // hashmap + spare + edata + size_t size = sizeof(struct hashmap)+bucketsz*2; + struct hashmap *map = _malloc(size); + if (!map) { + return NULL; + } + memset(map, 0, sizeof(struct hashmap)); + map->elsize = elsize; + map->bucketsz = bucketsz; + map->seed0 = seed0; + map->seed1 = seed1; + map->hash = hash; + map->compare = compare; + map->elfree = elfree; + map->udata = udata; + map->spare = ((char*)map)+sizeof(struct hashmap); + map->edata = (char*)map->spare+bucketsz; + map->cap = cap; + map->nbuckets = cap; + map->mask = map->nbuckets-1; + map->buckets = _malloc(map->bucketsz*map->nbuckets); + if (!map->buckets) { + _free(map); + return NULL; + } + memset(map->buckets, 0, map->bucketsz*map->nbuckets); + map->growat = map->nbuckets*0.75; + map->shrinkat = map->nbuckets*0.10; + map->malloc = _malloc; + map->realloc = _realloc; + map->free = _free; + return map; +} + + +// hashmap_new returns a new hash map. +// Param `elsize` is the size of each element in the tree. Every element that +// is inserted, deleted, or retrieved will be this size. +// Param `cap` is the default lower capacity of the hashmap. Setting this to +// zero will default to 16. +// Params `seed0` and `seed1` are optional seed values that are passed to the +// following `hash` function. These can be any value you wish but it's often +// best to use randomly generated values. +// Param `hash` is a function that generates a hash value for an item. It's +// important that you provide a good hash function, otherwise it will perform +// poorly or be vulnerable to Denial-of-service attacks. This implementation +// comes with two helper functions `hashmap_sip()` and `hashmap_murmur()`. +// Param `compare` is a function that compares items in the tree. See the +// qsort stdlib function for an example of how this function works. +// The hashmap must be freed with hashmap_free(). +// Param `elfree` is a function that frees a specific item. This should be NULL +// unless you're storing some kind of reference data in the hash. +struct hashmap *hashmap_new(size_t elsize, size_t cap, + uint64_t seed0, uint64_t seed1, + uint64_t (*hash)(const void *item, + uint64_t seed0, uint64_t seed1), + int (*compare)(const void *a, const void *b, + void *udata), + void (*elfree)(void *item), + void *udata) +{ + return hashmap_new_with_allocator( + (_malloc?_malloc:malloc), + (_realloc?_realloc:realloc), + (_free?_free:free), + elsize, cap, seed0, seed1, hash, compare, elfree, udata + ); +} + +static void free_elements(struct hashmap *map) { + if (map->elfree) { + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib) map->elfree(bucket_item(bucket)); + } + } +} + + +// hashmap_clear quickly clears the map. +// Every item is called with the element-freeing function given in hashmap_new, +// if present, to free any data referenced in the elements of the hashmap. +// When the update_cap is provided, the map's capacity will be updated to match +// the currently number of allocated buckets. This is an optimization to ensure +// that this operation does not perform any allocations. +void hashmap_clear(struct hashmap *map, bool update_cap) { + map->count = 0; + free_elements(map); + if (update_cap) { + map->cap = map->nbuckets; + } else if (map->nbuckets != map->cap) { + void *new_buckets = map->malloc(map->bucketsz*map->cap); + if (new_buckets) { + map->free(map->buckets); + map->buckets = new_buckets; + } + map->nbuckets = map->cap; + } + memset(map->buckets, 0, map->bucketsz*map->nbuckets); + map->mask = map->nbuckets-1; + map->growat = map->nbuckets*0.75; + map->shrinkat = map->nbuckets*0.10; +} + + +static bool resize(struct hashmap *map, size_t new_cap) { + struct hashmap *map2 = hashmap_new(map->elsize, new_cap, map->seed1, + map->seed1, map->hash, map->compare, + map->elfree, map->udata); + if (!map2) { + return false; + } + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *entry = bucket_at(map, i); + if (!entry->dib) { + continue; + } + entry->dib = 1; + size_t j = entry->hash & map2->mask; + for (;;) { + struct bucket *bucket = bucket_at(map2, j); + if (bucket->dib == 0) { + memcpy(bucket, entry, map->bucketsz); + break; + } + if (bucket->dib < entry->dib) { + memcpy(map2->spare, bucket, map->bucketsz); + memcpy(bucket, entry, map->bucketsz); + memcpy(entry, map2->spare, map->bucketsz); + } + j = (j + 1) & map2->mask; + entry->dib += 1; + } + } + map->free(map->buckets); + map->buckets = map2->buckets; + map->nbuckets = map2->nbuckets; + map->mask = map2->mask; + map->growat = map2->growat; + map->shrinkat = map2->shrinkat; + map->free(map2); + return true; +} + +// hashmap_set inserts or replaces an item in the hash map. If an item is +// replaced then it is returned otherwise NULL is returned. This operation +// may allocate memory. If the system is unable to allocate additional +// memory then NULL is returned and hashmap_oom() returns true. +void *hashmap_set(struct hashmap *map, void *item) { + if (!item) { + panic("item is null"); + } + map->oom = false; + if (map->count == map->growat) { + if (!resize(map, map->nbuckets*2)) { + map->oom = true; + return NULL; + } + } + + + struct bucket *entry = map->edata; + entry->hash = get_hash(map, item); + entry->dib = 1; + memcpy(bucket_item(entry), item, map->elsize); + + size_t i = entry->hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib == 0) { + memcpy(bucket, entry, map->bucketsz); + map->count++; + return NULL; + } + if (entry->hash == bucket->hash && + map->compare(bucket_item(entry), bucket_item(bucket), + map->udata) == 0) + { + memcpy(map->spare, bucket_item(bucket), map->elsize); + memcpy(bucket_item(bucket), bucket_item(entry), map->elsize); + return map->spare; + } + if (bucket->dib < entry->dib) { + memcpy(map->spare, bucket, map->bucketsz); + memcpy(bucket, entry, map->bucketsz); + memcpy(entry, map->spare, map->bucketsz); + } + i = (i + 1) & map->mask; + entry->dib += 1; + } +} + +// hashmap_get returns the item based on the provided key. If the item is not +// found then NULL is returned. +void *hashmap_get(struct hashmap *map, const void *key) { + if (!key) { + panic("key is null"); + } + uint64_t hash = get_hash(map, key); + size_t i = hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + if (bucket->hash == hash && + map->compare(key, bucket_item(bucket), map->udata) == 0) + { + return bucket_item(bucket); + } + i = (i + 1) & map->mask; + } +} + +// hashmap_probe returns the item in the bucket at position or NULL if an item +// is not set for that bucket. The position is 'moduloed' by the number of +// buckets in the hashmap. +void *hashmap_probe(struct hashmap *map, uint64_t position) { + size_t i = position & map->mask; + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + return bucket_item(bucket); +} + + +// hashmap_delete removes an item from the hash map and returns it. If the +// item is not found then NULL is returned. +void *hashmap_delete(struct hashmap *map, void *key) { + if (!key) { + panic("key is null"); + } + map->oom = false; + uint64_t hash = get_hash(map, key); + size_t i = hash & map->mask; + for (;;) { + struct bucket *bucket = bucket_at(map, i); + if (!bucket->dib) { + return NULL; + } + if (bucket->hash == hash && + map->compare(key, bucket_item(bucket), map->udata) == 0) + { + memcpy(map->spare, bucket_item(bucket), map->elsize); + bucket->dib = 0; + for (;;) { + struct bucket *prev = bucket; + i = (i + 1) & map->mask; + bucket = bucket_at(map, i); + if (bucket->dib <= 1) { + prev->dib = 0; + break; + } + memcpy(prev, bucket, map->bucketsz); + prev->dib--; + } + map->count--; + if (map->nbuckets > map->cap && map->count <= map->shrinkat) { + // Ignore the return value. It's ok for the resize operation to + // fail to allocate enough memory because a shrink operation + // does not change the integrity of the data. + resize(map, map->nbuckets/2); + } + return map->spare; + } + i = (i + 1) & map->mask; + } +} + +// hashmap_count returns the number of items in the hash map. +size_t hashmap_count(struct hashmap *map) { + return map->count; +} + +// hashmap_free frees the hash map +// Every item is called with the element-freeing function given in hashmap_new, +// if present, to free any data referenced in the elements of the hashmap. +void hashmap_free(struct hashmap *map) { + if (!map) return; + free_elements(map); + map->free(map->buckets); + map->free(map); +} + +// hashmap_oom returns true if the last hashmap_set() call failed due to the +// system being out of memory. +bool hashmap_oom(struct hashmap *map) { + return map->oom; +} + +// hashmap_scan iterates over all items in the hash map +// Param `iter` can return false to stop iteration early. +// Returns false if the iteration has been stopped early. +bool hashmap_scan(struct hashmap *map, + bool (*iter)(const void *item, void *udata), void *udata) +{ + for (size_t i = 0; i < map->nbuckets; i++) { + struct bucket *bucket = bucket_at(map, i); + if (bucket->dib) { + if (!iter(bucket_item(bucket), udata)) { + return false; + } + } + } + return true; +} + +//----------------------------------------------------------------------------- +// SipHash reference C implementation +// +// Copyright (c) 2012-2016 Jean-Philippe Aumasson +// +// Copyright (c) 2012-2014 Daniel J. Bernstein +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// . +// +// default: SipHash-2-4 +//----------------------------------------------------------------------------- +static uint64_t SIP64(const uint8_t *in, const size_t inlen, + uint64_t seed0, uint64_t seed1) +{ +#define U8TO64_LE(p) \ + { (((uint64_t)((p)[0])) | ((uint64_t)((p)[1]) << 8) | \ + ((uint64_t)((p)[2]) << 16) | ((uint64_t)((p)[3]) << 24) | \ + ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) | \ + ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56)) } +#define U64TO8_LE(p, v) \ + { U32TO8_LE((p), (uint32_t)((v))); \ + U32TO8_LE((p) + 4, (uint32_t)((v) >> 32)); } +#define U32TO8_LE(p, v) \ + { (p)[0] = (uint8_t)((v)); \ + (p)[1] = (uint8_t)((v) >> 8); \ + (p)[2] = (uint8_t)((v) >> 16); \ + (p)[3] = (uint8_t)((v) >> 24); } +#define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b)))) +#define SIPROUND \ + { v0 += v1; v1 = ROTL(v1, 13); \ + v1 ^= v0; v0 = ROTL(v0, 32); \ + v2 += v3; v3 = ROTL(v3, 16); \ + v3 ^= v2; \ + v0 += v3; v3 = ROTL(v3, 21); \ + v3 ^= v0; \ + v2 += v1; v1 = ROTL(v1, 17); \ + v1 ^= v2; v2 = ROTL(v2, 32); } + uint64_t k0 = U8TO64_LE((uint8_t*)&seed0); + uint64_t k1 = U8TO64_LE((uint8_t*)&seed1); + uint64_t v3 = UINT64_C(0x7465646279746573) ^ k1; + uint64_t v2 = UINT64_C(0x6c7967656e657261) ^ k0; + uint64_t v1 = UINT64_C(0x646f72616e646f6d) ^ k1; + uint64_t v0 = UINT64_C(0x736f6d6570736575) ^ k0; + const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t)); + for (; in != end; in += 8) { + uint64_t m = U8TO64_LE(in); + v3 ^= m; + SIPROUND; SIPROUND; + v0 ^= m; + } + const int left = inlen & 7; + uint64_t b = ((uint64_t)inlen) << 56; + switch (left) { + case 7: b |= ((uint64_t)in[6]) << 48; + case 6: b |= ((uint64_t)in[5]) << 40; + case 5: b |= ((uint64_t)in[4]) << 32; + case 4: b |= ((uint64_t)in[3]) << 24; + case 3: b |= ((uint64_t)in[2]) << 16; + case 2: b |= ((uint64_t)in[1]) << 8; + case 1: b |= ((uint64_t)in[0]); break; + case 0: break; + } + v3 ^= b; + SIPROUND; SIPROUND; + v0 ^= b; + v2 ^= 0xff; + SIPROUND; SIPROUND; SIPROUND; SIPROUND; + b = v0 ^ v1 ^ v2 ^ v3; + uint64_t out = 0; + U64TO8_LE((uint8_t*)&out, b); + return out; +} + +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. +// +// Murmur3_86_128 +//----------------------------------------------------------------------------- +static void MM86128(const void *key, const int len, uint32_t seed, void *out) { +#define ROTL32(x, r) ((x << r) | (x >> (32 - r))) +#define FMIX32(h) h^=h>>16; h*=0x85ebca6b; h^=h>>13; h*=0xc2b2ae35; h^=h>>16; + const uint8_t * data = (const uint8_t*)key; + const int nblocks = len / 16; + uint32_t h1 = seed; + uint32_t h2 = seed; + uint32_t h3 = seed; + uint32_t h4 = seed; + uint32_t c1 = 0x239b961b; + uint32_t c2 = 0xab0e9789; + uint32_t c3 = 0x38b34ae5; + uint32_t c4 = 0xa1e38b93; + const uint32_t * blocks = (const uint32_t *)(data + nblocks*16); + for (int i = -nblocks; i; i++) { + uint32_t k1 = blocks[i*4+0]; + uint32_t k2 = blocks[i*4+1]; + uint32_t k3 = blocks[i*4+2]; + uint32_t k4 = blocks[i*4+3]; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b; + k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; + h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747; + k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; + h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35; + k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; + h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17; + } + const uint8_t * tail = (const uint8_t*)(data + nblocks*16); + uint32_t k1 = 0; + uint32_t k2 = 0; + uint32_t k3 = 0; + uint32_t k4 = 0; + switch(len & 15) { + case 15: k4 ^= tail[14] << 16; + case 14: k4 ^= tail[13] << 8; + case 13: k4 ^= tail[12] << 0; + k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; + case 12: k3 ^= tail[11] << 24; + case 11: k3 ^= tail[10] << 16; + case 10: k3 ^= tail[ 9] << 8; + case 9: k3 ^= tail[ 8] << 0; + k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; + case 8: k2 ^= tail[ 7] << 24; + case 7: k2 ^= tail[ 6] << 16; + case 6: k2 ^= tail[ 5] << 8; + case 5: k2 ^= tail[ 4] << 0; + k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; + case 4: k1 ^= tail[ 3] << 24; + case 3: k1 ^= tail[ 2] << 16; + case 2: k1 ^= tail[ 1] << 8; + case 1: k1 ^= tail[ 0] << 0; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + }; + h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len; + h1 += h2; h1 += h3; h1 += h4; + h2 += h1; h3 += h1; h4 += h1; + FMIX32(h1); FMIX32(h2); FMIX32(h3); FMIX32(h4); + h1 += h2; h1 += h3; h1 += h4; + h2 += h1; h3 += h1; h4 += h1; + ((uint32_t*)out)[0] = h1; + ((uint32_t*)out)[1] = h2; + ((uint32_t*)out)[2] = h3; + ((uint32_t*)out)[3] = h4; +} + +// hashmap_sip returns a hash value for `data` using SipHash-2-4. +uint64_t hashmap_sip(const void *data, size_t len, + uint64_t seed0, uint64_t seed1) +{ + return SIP64((uint8_t*)data, len, seed0, seed1); +} + +// hashmap_murmur returns a hash value for `data` using Murmur3_86_128. +uint64_t hashmap_murmur(const void *data, size_t len, + uint64_t seed0, uint64_t seed1) +{ + char out[16]; + MM86128(data, len, seed0, &out); + return *(uint64_t*)out; +} + +//============================================================================== +// TESTS AND BENCHMARKS +// $ cc -DHASHMAP_TEST hashmap.c && ./a.out # run tests +// $ cc -DHASHMAP_TEST -O3 hashmap.c && BENCH=1 ./a.out # run benchmarks +//============================================================================== +#ifdef HASHMAP_TEST + +static size_t deepcount(struct hashmap *map) { + size_t count = 0; + for (size_t i = 0; i < map->nbuckets; i++) { + if (bucket_at(map, i)->dib) { + count++; + } + } + return count; +} + + +#pragma GCC diagnostic ignored "-Wextra" + + +#include +#include +#include +#include +#include +#include "hashmap.h" + +static bool rand_alloc_fail = false; +static int rand_alloc_fail_odds = 3; // 1 in 3 chance malloc will fail. +static uintptr_t total_allocs = 0; +static uintptr_t total_mem = 0; + +static void *xmalloc(size_t size) { + if (rand_alloc_fail && rand()%rand_alloc_fail_odds == 0) { + return NULL; + } + void *mem = malloc(sizeof(uintptr_t)+size); + assert(mem); + *(uintptr_t*)mem = size; + total_allocs++; + total_mem += size; + return (char*)mem+sizeof(uintptr_t); +} + +static void xfree(void *ptr) { + if (ptr) { + total_mem -= *(uintptr_t*)((char*)ptr-sizeof(uintptr_t)); + free((char*)ptr-sizeof(uintptr_t)); + total_allocs--; + } +} + +static void shuffle(void *array, size_t numels, size_t elsize) { + char tmp[elsize]; + char *arr = array; + for (size_t i = 0; i < numels - 1; i++) { + int j = i + rand() / (RAND_MAX / (numels - i) + 1); + memcpy(tmp, arr + j * elsize, elsize); + memcpy(arr + j * elsize, arr + i * elsize, elsize); + memcpy(arr + i * elsize, tmp, elsize); + } +} + +static bool iter_ints(const void *item, void *udata) { + int *vals = *(int**)udata; + vals[*(int*)item] = 1; + return true; +} + +static int compare_ints(const void *a, const void *b) { + return *(int*)a - *(int*)b; +} + +static int compare_ints_udata(const void *a, const void *b, void *udata) { + return *(int*)a - *(int*)b; +} + +static int compare_strs(const void *a, const void *b, void *udata) { + return strcmp(*(char**)a, *(char**)b); +} + +static uint64_t hash_int(const void *item, uint64_t seed0, uint64_t seed1) { + return hashmap_murmur(item, sizeof(int), seed0, seed1); +} + +static uint64_t hash_str(const void *item, uint64_t seed0, uint64_t seed1) { + return hashmap_murmur(*(char**)item, strlen(*(char**)item), seed0, seed1); +} + +static void free_str(void *item) { + xfree(*(char**)item); +} + +static void all() { + int seed = getenv("SEED")?atoi(getenv("SEED")):time(NULL); + int N = getenv("N")?atoi(getenv("N")):2000; + printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int)); + srand(seed); + + rand_alloc_fail = true; + + // test sip and murmur hashes + assert(hashmap_sip("hello", 5, 1, 2) == 2957200328589801622); + assert(hashmap_murmur("hello", 5, 1, 2) == 1682575153221130884); + + int *vals; + while (!(vals = xmalloc(N * sizeof(int)))) {} + for (int i = 0; i < N; i++) { + vals[i] = i; + } + + struct hashmap *map; + + while (!(map = hashmap_new(sizeof(int), 0, seed, seed, + hash_int, compare_ints_udata, NULL, NULL))) {} + shuffle(vals, N, sizeof(int)); + for (int i = 0; i < N; i++) { + // // printf("== %d ==\n", vals[i]); + assert(map->count == i); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + int *v; + assert(!hashmap_get(map, &vals[i])); + assert(!hashmap_delete(map, &vals[i])); + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + + for (int j = 0; j < i; j++) { + v = hashmap_get(map, &vals[j]); + assert(v && *v == vals[j]); + } + while (true) { + v = hashmap_set(map, &vals[i]); + if (!v) { + assert(hashmap_oom(map)); + continue; + } else { + assert(!hashmap_oom(map)); + assert(v && *v == vals[i]); + break; + } + } + v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + assert(!hashmap_get(map, &vals[i])); + assert(!hashmap_delete(map, &vals[i])); + assert(!hashmap_set(map, &vals[i])); + assert(map->count == i+1); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + } + + int *vals2; + while (!(vals2 = xmalloc(N * sizeof(int)))) {} + memset(vals2, 0, N * sizeof(int)); + assert(hashmap_scan(map, iter_ints, &vals2)); + for (int i = 0; i < N; i++) { + assert(vals2[i] == 1); + } + xfree(vals2); + + shuffle(vals, N, sizeof(int)); + for (int i = 0; i < N; i++) { + int *v; + v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + assert(!hashmap_get(map, &vals[i])); + assert(map->count == N-i-1); + assert(map->count == hashmap_count(map)); + assert(map->count == deepcount(map)); + for (int j = N-1; j > i; j--) { + v = hashmap_get(map, &vals[j]); + assert(v && *v == vals[j]); + } + } + + for (int i = 0; i < N; i++) { + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + } + + assert(map->count != 0); + size_t prev_cap = map->cap; + hashmap_clear(map, true); + assert(prev_cap < map->cap); + assert(map->count == 0); + + + for (int i = 0; i < N; i++) { + while (true) { + assert(!hashmap_set(map, &vals[i])); + if (!hashmap_oom(map)) { + break; + } + } + } + + prev_cap = map->cap; + hashmap_clear(map, false); + assert(prev_cap == map->cap); + + hashmap_free(map); + + xfree(vals); + + + while (!(map = hashmap_new(sizeof(char*), 0, seed, seed, + hash_str, compare_strs, free_str, NULL))); + + for (int i = 0; i < N; i++) { + char *str; + while (!(str = xmalloc(16))); + sprintf(str, "s%i", i); + while(!hashmap_set(map, &str)); + } + + hashmap_clear(map, false); + assert(hashmap_count(map) == 0); + + for (int i = 0; i < N; i++) { + char *str; + while (!(str = xmalloc(16))); + sprintf(str, "s%i", i); + while(!hashmap_set(map, &str)); + } + + hashmap_free(map); + + if (total_allocs != 0) { + fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs); + exit(1); + } +} + +#define bench(name, N, code) {{ \ + if (strlen(name) > 0) { \ + printf("%-14s ", name); \ + } \ + size_t tmem = total_mem; \ + size_t tallocs = total_allocs; \ + uint64_t bytes = 0; \ + clock_t begin = clock(); \ + for (int i = 0; i < N; i++) { \ + (code); \ + } \ + clock_t end = clock(); \ + double elapsed_secs = (double)(end - begin) / CLOCKS_PER_SEC; \ + double bytes_sec = (double)bytes/elapsed_secs; \ + printf("%d ops in %.3f secs, %.0f ns/op, %.0f op/sec", \ + N, elapsed_secs, \ + elapsed_secs/(double)N*1e9, \ + (double)N/elapsed_secs \ + ); \ + if (bytes > 0) { \ + printf(", %.1f GB/sec", bytes_sec/1024/1024/1024); \ + } \ + if (total_mem > tmem) { \ + size_t used_mem = total_mem-tmem; \ + printf(", %.2f bytes/op", (double)used_mem/N); \ + } \ + if (total_allocs > tallocs) { \ + size_t used_allocs = total_allocs-tallocs; \ + printf(", %.2f allocs/op", (double)used_allocs/N); \ + } \ + printf("\n"); \ +}} + +static void benchmarks() { + int seed = getenv("SEED")?atoi(getenv("SEED")):time(NULL); + int N = getenv("N")?atoi(getenv("N")):5000000; + printf("seed=%d, count=%d, item_size=%zu\n", seed, N, sizeof(int)); + srand(seed); + + + int *vals = xmalloc(N * sizeof(int)); + for (int i = 0; i < N; i++) { + vals[i] = i; + } + + shuffle(vals, N, sizeof(int)); + + struct hashmap *map; + shuffle(vals, N, sizeof(int)); + + map = hashmap_new(sizeof(int), 0, seed, seed, hash_int, compare_ints_udata, + NULL, NULL); + bench("set", N, { + int *v = hashmap_set(map, &vals[i]); + assert(!v); + }) + shuffle(vals, N, sizeof(int)); + bench("get", N, { + int *v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + }) + shuffle(vals, N, sizeof(int)); + bench("delete", N, { + int *v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + }) + hashmap_free(map); + + map = hashmap_new(sizeof(int), N, seed, seed, hash_int, compare_ints_udata, + NULL, NULL); + bench("set (cap)", N, { + int *v = hashmap_set(map, &vals[i]); + assert(!v); + }) + shuffle(vals, N, sizeof(int)); + bench("get (cap)", N, { + int *v = hashmap_get(map, &vals[i]); + assert(v && *v == vals[i]); + }) + shuffle(vals, N, sizeof(int)); + bench("delete (cap)" , N, { + int *v = hashmap_delete(map, &vals[i]); + assert(v && *v == vals[i]); + }) + + hashmap_free(map); + + + xfree(vals); + + if (total_allocs != 0) { + fprintf(stderr, "total_allocs: expected 0, got %lu\n", total_allocs); + exit(1); + } +} + +int main() { + hashmap_set_allocator(xmalloc, xfree); + + if (getenv("BENCH")) { + printf("Running hashmap.c benchmarks...\n"); + benchmarks(); + } else { + printf("Running hashmap.c tests...\n"); + all(); + printf("PASSED\n"); + } +} + + +#endif + + + diff --git a/lib/src/lerror.c b/lib/src/lerror.c new file mode 100644 index 0000000..79c15bc --- /dev/null +++ b/lib/src/lerror.c @@ -0,0 +1,4 @@ +#include "lerror.h" + +jmp_buf eLaika_errStack[LAIKA_MAXERRORS]; +int eLaika_errIndx = -1; \ No newline at end of file diff --git a/lib/src/lmem.c b/lib/src/lmem.c new file mode 100644 index 0000000..3d5c591 --- /dev/null +++ b/lib/src/lmem.c @@ -0,0 +1,18 @@ +#include "lerror.h" +#include "lmem.h" + +void *laikaM_realloc(void *buf, size_t sz) { + void *newBuf; + + /* are we free'ing the buffer? */ + if (sz == 0) { + free(buf); + return NULL; + } + + /* if NULL is passed, realloc() acts like malloc() */ + if ((newBuf = realloc(buf, sz)) == NULL) + CERROR("failed to allocate memory!"); + + return newBuf; +} \ No newline at end of file diff --git a/lib/src/lpeer.c b/lib/src/lpeer.c new file mode 100644 index 0000000..93ee734 --- /dev/null +++ b/lib/src/lpeer.c @@ -0,0 +1,88 @@ +#include "lerror.h" +#include "lmem.h" +#include "lpeer.h" + +struct sLaika_peer *laikaS_newPeer(void (*pktHandler)(struct sLaika_peer *peer, LAIKAPKT_ID id), struct sLaika_pollList *pList, size_t *pktSizeTable) { + struct sLaika_peer *peer = laikaM_malloc(sizeof(struct sLaika_peer)); + + laikaS_initSocket(&peer->sock); + peer->pktHandler = pktHandler; + peer->pList = pList; + peer->pktSizeTable = pktSizeTable; + peer->pktSize = 0; + peer->pktID = LAIKAPKT_MAXNONE; + peer->setPollOut = false; + return peer; +} + +void laikaS_freePeer(struct sLaika_peer *peer) { + laikaS_cleanSocket(&peer->sock); + laikaM_free(peer); +} + +bool laikaS_handlePeerIn(struct sLaika_peer *peer) { + RAWSOCKCODE err; + int recvd; + bool _tryCatchRes; + + switch (peer->pktID) { + case LAIKAPKT_MAXNONE: + /* try grabbing pktID */ + if (laikaS_rawRecv(&peer->sock, sizeof(uint8_t), &recvd) != RAWSOCK_OK) + return false; + + peer->pktID = laikaS_readByte(&peer->sock); + + /* sanity check packet ID */ + if (peer->pktID >= LAIKAPKT_MAXNONE) + CERROR("received evil pktID!") + + peer->pktSize = peer->pktSizeTable[peer->pktID]; + break; + default: + /* try grabbing the rest of the packet */ + if (laikaS_rawRecv(&peer->sock, peer->pktSize - peer->sock.inCount, &recvd) != RAWSOCK_OK) + return false; + + /* have we received the full packet? */ + if (peer->pktSize == peer->sock.inCount) { + /* dispatch to packet handler */ + LAIKA_TRY + peer->pktHandler(peer, peer->pktID); + _tryCatchRes = true; + LAIKA_CATCH + _tryCatchRes = false; + LAIKA_TRYEND /* can't skip this, so the return is after */ + + return _tryCatchRes; + } + } +} + +bool laikaS_handlePeerOut(struct sLaika_peer *peer) { + RAWSOCKCODE err; + int sent; + + if (peer->sock.outCount == 0) /* sanity check */ + return; + + switch (laikaS_rawSend(&peer->sock, peer->sock.outCount, &sent)) { + case RAWSOCK_OK: /* we're ok! */ + if (peer->setPollOut) { /* if POLLOUT was set, unset it */ + laikaP_rmvPollOut(peer->pList, &peer->sock); + peer->setPollOut = false; + } + return true; + case RAWSOCK_POLL: /* we've been asked to set the POLLOUT flag */ + if (!peer->setPollOut) { /* if POLLOUT wasn't set, set it so we'll be notified whenever the kernel has room :) */ + laikaP_addPollOut(peer->pList, &peer->sock); + peer->setPollOut = true; + } + return true; + default: /* panic! */ + case RAWSOCK_CLOSED: + case RAWSOCK_ERROR: + return false; + } + +} \ No newline at end of file diff --git a/lib/src/lpolllist.c b/lib/src/lpolllist.c new file mode 100644 index 0000000..585871c --- /dev/null +++ b/lib/src/lpolllist.c @@ -0,0 +1,191 @@ +#include "lerror.h" +#include "lmem.h" +#include "lpolllist.h" + +typedef struct sLaika_hashMapElem { + SOCKET fd; + struct sLaika_socket *sock; +} tLaika_hashMapElem; + +int elem_compare(const void *a, const void *b, void *udata) { + const tLaika_hashMapElem *ua = a; + const tLaika_hashMapElem *ub = b; + + return ua->fd != ub->fd; +} + +uint64_t elem_hash(const void *item, uint64_t seed0, uint64_t seed1) { + const tLaika_hashMapElem *u = item; + return (uint64_t)(u->fd); +} + +void laikaP_initPList(struct sLaika_pollList *pList) { + laikaS_init(); + + /* setup hashmap */ + pList->sockets = hashmap_new(sizeof(tLaika_hashMapElem), POLLSTARTCAP, 0, 0, elem_hash, elem_compare, NULL, NULL); + pList->revents = NULL; /* laikaP_pollList() will allocate the buffer */ + pList->reventCapacity = POLLSTARTCAP/GROW_FACTOR; + pList->reventCount = 0; + +#ifdef LAIKA_USE_EPOLL + /* setup our epoll */ + memset(&pList->ev, 0, sizeof(struct epoll_event)); + if ((pList->epollfd = epoll_create(POLLSTARTCAP)) == -1) + CERROR("epoll_create() failed!"); + +#else + pList->fds = NULL; /* laikaP_addSock will allocate the buffer */ + pList->fdCapacity = POLLSTARTCAP/GROW_FACTOR; /* div by GROW_FACTOR since laikaM_growarray multiplies by GROW_FACTOR */ + pList->fdCount = 0; +#endif +} + +void laikaP_cleanPList(struct sLaika_pollList *pList) { + laikaM_free(pList->revents); + +#ifdef LAIKA_USE_EPOLL + close(pList->epollfd); +#else + laikaM_free(pList->fds); +#endif + + laikaS_cleanUp(); +} + +void laikaP_addSock(struct sLaika_pollList *pList, struct sLaika_socket *sock) { + /* add socket to hashmap */ + hashmap_set(pList->sockets, &(tLaika_hashMapElem){.fd = sock->sock, .sock = sock}); + +#ifdef LAIKA_USE_EPOLL + pList->ev.events = EPOLLIN; + pList->ev.data.ptr = (void*)sock; + + if (epoll_ctl(pList->epollfd, EPOLL_CTL_ADD, sock->sock, &pList->ev) == -1) + CERROR("epoll_ctl [ADD] failed"); + +#else + /* allocate space in array & add PollFD */ + laikaM_growarray(PollFD, pList->fds, pList->fdCount, pList->fdCapacity); + pList->fds[pList->fdCount++] = (PollFD){sock->sock, POLLIN}; +#endif +} + +void laikaP_rmvSock(struct sLaika_pollList *pList, struct sLaika_socket *sock) { + /* remove socket from hashmap */ + hashmap_delete(pList->sockets, &(tLaika_hashMapElem){.fd = sock->sock, .sock = sock}); + +#ifdef LAIKA_USE_EPOLL + /* epoll_event* isn't needed with EPOLL_CTL_DEL, however we still need to pass a NON-NULL pointer. [see: https://man7.org/linux/man-pages/man2/epoll_ctl.2.html#BUGS] */ + if (epoll_ctl(pList->epollfd, EPOLL_CTL_DEL, sock->sock, &pList->ev) == -1) { + /* non-fatal error, socket probably just didn't exist, so ignore it. */ + CWARN("epoll_ctl [DEL] failed"); + } +#else + int i; + + /* search fds for socket, remove it and shrink array */ + for (i = 0; i < pList->fdCount; i++) { + if (pList->fds[i].fd == sock->sock) { + /* remove from array */ + laikaM_rmvarray(PollFD, pList->fds, pList->fdCount, i, 1); + break; + } + } +#endif +} + +void laikaP_addPollOut(struct sLaika_pollList *pList, struct sLaika_socket *sock) { +#ifdef LAIKA_USE_EPOLL + pList->ev.events = EPOLLIN | EPOLLOUT; + pList->ev.data.ptr = (void*)sock; + if (epoll_ctl(pList->epollfd, EPOLL_CTL_MOD, sock->sock, &pList->ev) == -1) { + /* non-fatal error, socket probably just didn't exist, so ignore it. */ + CWARN("epoll_ctl [MOD] failed"); + } +#else + int i; + + /* search fds for socket, add POLLOUT flag */ + for (i = 0; i < pList->fdCount; i++) { + if (pList->fds[i].fd == sock->sock) { + pList->fds[i].events = POLLIN | POLLOUT; + break; + } + } +#endif +} + +void laikaP_rmvPollOut(struct sLaika_pollList *pList, struct sLaika_socket *sock) { +#ifdef LAIKA_USE_EPOLL + pList->ev.events = EPOLLIN; + pList->ev.data.ptr = (void*)sock; + if (epoll_ctl(pList->epollfd, EPOLL_CTL_MOD, sock->sock, &pList->ev) == -1) { + /* non-fatal error, socket probably just didn't exist, so ignore it. */ + CWARN("epoll_ctl [MOD] failed"); + } +#else + int i; + + /* search fds for socket, remove POLLOUT flag */ + for (i = 0; i < pList->fdCount; i++) { + if (pList->fds[i].fd == sock->sock) { + pList->fds[i].events = POLLIN; + break; + } + } +#endif +} + +struct sLaika_pollEvent *laikaP_poll(struct sLaika_pollList *pList, int timeout, int *_nevents) { + int nEvents, i; + + pList->reventCount = 0; /* reset revent array */ +#ifdef LAIKA_USE_EPOLL +/* fastpath: we store the sLaika_socket* pointer directly in the epoll_data_t, saving us a lookup into our socket hashmap + not to mention the various improvements epoll() has over poll() :D +*/ + nEvents = epoll_wait(pList->epollfd, pList->ep_events, MAX_EPOLL_EVENTS, timeout); + + if (SOCKETERROR(nEvents)) + CERROR("epoll_wait() failed!"); + + for (i = 0; i < nEvents; i++) { + /* add event to revent array */ + laikaM_growarray(struct sLaika_pollEvent, pList->revents, pList->reventCount, pList->reventCapacity); + pList->revents[pList->reventCount++] = (struct sLaika_pollEvent){ + .sock = pList->ep_events[i].data.ptr, + .pollIn = pList->ep_events[i].events & EPOLLIN, + .pollOut = pList->ep_events[i].events & EPOLLOUT + }; + } +#else + nEvents = poll(pList->fds, pList->fdCount, timeout); /* poll returns -1 for error, or the number of events */ + + if (SOCKETERROR(nEvents)) + CERROR("poll() failed!"); + + /* walk through the returned poll fds, if they have an event, add it to our revents array */ + for (i = 0; i < pList->fdCount && nEvents > 0; i++) { + PollFD pfd = pList->fds[i]; + if (pList->fds[i].revents != 0) { + /* grab socket from hashmap */ + struct sLaika_socket *sock = hashmap_get(pList->sockets, &(tLaika_hashMapElem){.fd = (SOCKET)pfd.fd}); + + /* insert event into revents array */ + laikaM_growarray(struct sLaika_pollEvent, pList->revents, pList->reventCount, pList->reventCapacity); + pList->revents[pList->reventCount++] = (struct sLaika_pollEvent){ + .sock = sock, + .pollIn = pfd.revents & POLLIN, + .pollOut = pfd.revents & POLLOUT + }; + + nEvents--; + } + } +#endif + + /* return revents array */ + *_nevents = i; + return pList->revents; +} diff --git a/lib/src/lsocket.c b/lib/src/lsocket.c new file mode 100644 index 0000000..97d0db6 --- /dev/null +++ b/lib/src/lsocket.c @@ -0,0 +1,254 @@ +#include "lerror.h" +#include "lmem.h" +#include "lpolllist.h" +#include "lsocket.h" + +static int _LNSetup = 0; + +void laikaS_init(void) { + if (_LNSetup++ > 0) + return; /* WSA is already setup! */ + +#ifdef _WIN32 + WSADATA wsaData; + + if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) + CERROR("WSAStartup failed!") +#endif +} + +void laikaS_cleanUp(void) { + if (--_LNSetup > 0) + return; /* WSA still needs to be up, a FoxNet peer is still using it */ + +#ifdef _WIN32 + WSACleanup(); +#endif +} + +void laikaS_initSocket(struct sLaika_socket *sock) { + sock->sock = INVALID_SOCKET; + sock->inBuf = NULL; + sock->inCap = ARRAY_START; + sock->inCount = 0; + sock->outBuf = NULL; + sock->outCap = ARRAY_START; + sock->outCount = 0; + + laikaS_init(); + return sock; +} + +void laikaS_cleanSocket(struct sLaika_socket *sock) { + /* free in & out arrays */ + laikaM_free(sock->inBuf); + laikaM_free(sock->outBuf); + + /* kill socket & cleanup WSA */ + laikaS_kill(sock); + laikaS_cleanUp(); +} + +void laikaS_kill(struct sLaika_socket *sock) { + if (!laikaS_isAlive(sock)) /* sanity check */ + return; + +#ifdef _WIN32 + shutdown(sock->sock, SD_BOTH); + closesocket(sock->sock); +#else + shutdown(sock->sock, SHUT_RDWR); + close(sock->sock); +#endif + + sock->sock = INVALID_SOCKET; +} + +void laikaS_connect(struct sLaika_socket *sock, char *ip, char *port) { + struct addrinfo res, *result, *curr; + + if (!SOCKETINVALID(sock->sock)) + CERROR("socket already setup!"); + + /* zero out our address info and setup the type */ + memset(&res, 0, sizeof(struct addrinfo)); + res.ai_family = AF_UNSPEC; + res.ai_socktype = SOCK_STREAM; + + /* grab the address info */ + if (getaddrinfo(ip, port, &res, &result) != 0) + CERROR("getaddrinfo() failed!"); + + /* getaddrinfo returns a list of possible addresses, step through them and try them until we find a valid address */ + for (curr = result; curr != NULL; curr = curr->ai_next) { + sock->sock = socket(curr->ai_family, curr->ai_socktype, curr->ai_protocol); + + /* if it failed, try the next sock */ + if (SOCKETINVALID(sock->sock)) + continue; + + /* if it's not an invalid socket, break and exit the loop, we found a working addr! */ + if (!SOCKETINVALID(connect(sock->sock, curr->ai_addr, curr->ai_addrlen))) + break; + + laikaS_kill(sock); + } + freeaddrinfo(result); + + /* if we reached the end of the linked list, we failed looking up the addr */ + if (curr == NULL) + CERROR("couldn't connect a valid address handle to socket!"); +} + +void laikaS_bind(struct sLaika_socket *sock, uint16_t port) { + socklen_t addressSize; + struct sockaddr_in address; + + if (!SOCKETINVALID(sock)) + CERROR("socket already setup!") + + /* open our socket */ + sock->sock = socket(AF_INET, SOCK_STREAM, 0); + if (SOCKETINVALID(sock)) + CERROR("socket() failed!"); + + /* attach socket to the port */ + int opt = 1; +#ifdef _WIN32 + if (setsockopt(sock->sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(int)) != 0) +#else + if (setsockopt(sock->sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) != 0) +#endif + CERROR("setsockopt() failed!"); + + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(port); + + addressSize = sizeof(struct sockaddr_in); + + /* bind to the port */ + if (SOCKETERROR(bind(sock->sock, (struct sockaddr *)&address, addressSize))) + CERROR("bind() failed!"); + + if (SOCKETERROR(listen(sock->sock, SOMAXCONN))) + CERROR("listen() failed!"); +} + +void laikaS_acceptFrom(struct sLaika_socket *sock, struct sLaika_socket *from) { + socklen_t addressSize; + struct sockaddr address; + + sock = accept(from->sock, &address, &addressSize); + if (SOCKETINVALID(sock)) + CERROR("accept() failed!") +} + +bool laikaS_setNonBlock(struct sLaika_socket *sock) { +#ifdef _WIN32 + unsigned long mode = 1; + if (ioctlsocket(sock->sock, FIONBIO, &mode) != 0) { +#else + if (fcntl(sock->sock, F_SETFL, (fcntl(sock->sock, F_GETFL, 0) | O_NONBLOCK)) != 0) { +#endif + CWARN("fcntl failed on new connection"); + laikaS_kill(sock); + return false; + } + + return true; +} + +void laikaS_read(struct sLaika_socket *sock, void *buf, size_t sz) { + memcpy(buf, sock->inBuf, sz); + laikaM_rmvarray(uint8_t, sock->inBuf, sock->inCount, 0, sz); +} + +void laikaS_write(struct sLaika_socket *sock, void *buf, size_t sz) { + /* make sure we have enough space to copy the buffer */ + laikaM_growarray(uint8_t, sock->outBuf, sock->outCount + sz, sock->outCap);\ + + /* copy the buffer, then increment outCount */ + memcpy(&sock->outBuf[sock->outCount], buf, sz); + sock->outCount += sz; +} + +void laikaS_writeByte(struct sLaika_socket *sock, uint8_t data) { + laikaM_growarray(uint8_t, sock->outBuf, sock->outCount, sock->outCap); + sock->outBuf[sock->outCount++] = data; +} + +uint8_t laikaS_readByte(struct sLaika_socket *sock) { + uint8_t tmp = *sock->inBuf; + + /* pop 1 byte */ + laikaM_rmvarray(uint8_t, sock->inBuf, sock->inCount, 0, 1); + return tmp; +} + +RAWSOCKCODE laikaS_rawRecv(struct sLaika_socket *sock, size_t sz, int *processed) { + RAWSOCKCODE errCode = RAWSOCK_OK; + int rcvd, start = sock->inCount; + + /* make sure we have enough space to recv */ + laikaM_growarray(uint8_t, sock->inBuf, sock->inCount + sz, sock->inCap); + rcvd = recv(sock->sock, (buffer_t*)&sock->inBuf[sock->inCount], sz, LN_MSG_NOSIGNAL); + + if (rcvd == 0) { + errCode = RAWSOCK_CLOSED; + } else if (SOCKETERROR(rcvd) && LN_ERRNO != LN_EWOULD +#ifndef _WIN32 + /* if it's a posix system, also make sure its not a EAGAIN result (which is a recoverable error, there's just nothing to read lol) */ + && LN_ERRNO != EAGAIN +#endif + ) { + /* if the socket closed or an error occurred, return the error result */ + errCode = RAWSOCK_ERROR; + } else if (rcvd > 0) { + /* recv() worked, add rcvd to inCount */ + sock->inCount += rcvd; + } + + *processed = rcvd; + return errCode; +} + +RAWSOCKCODE laikaS_rawSend(struct sLaika_socket *sock, size_t sz, int *processed) { + RAWSOCKCODE errCode = RAWSOCK_OK; + int sent, sentBytes = 0; + + /* write bytes to the socket until an error occurs or we finish sending */ + do { + sent = send(sock->sock, (buffer_t*)(&sock->outBuf[sentBytes]), sz - sentBytes, LN_MSG_NOSIGNAL); + + /* check for error result */ + if (sent == 0) { /* connection closed gracefully */ + errCode = RAWSOCK_CLOSED; + goto _rawWriteExit; + } else if (SOCKETERROR(sent)) { /* socket error? */ + if (LN_ERRNO != LN_EWOULD +#ifndef _WIN32 + /* posix also has some platforms which define EAGAIN as a different value than EWOULD, might as well support it. */ + && LN_ERRNO != EAGAIN +#endif + ) { /* socket error! */ + errCode = RAWSOCK_ERROR; + goto _rawWriteExit; + } + + /* + it was a result of EWOULD or EAGAIN, kernel socket send buffer is full, + tell the caller we need to set our poll event POLLOUT + */ + errCode = RAWSOCK_POLL; + goto _rawWriteExit; + } + } while((sentBytes += sent) < sz); + +_rawWriteExit: + /* trim sent data from outBuf */ + laikaM_rmvarray(uint8_t, sock->outBuf, sock->outCount, 0, sentBytes); + + *processed = sentBytes; + return errCode; +} \ No newline at end of file