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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 9 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
- `sp_da`, a dynamic array
- `sp_ht`, hash tables with arbitrary keys and values (including strings)
- `sp_ps`, subprocesses
- `sp_io`, synchronous IO on top of files and buffers
- File monitors, [beautiful, interactive CLI prompts](https://spader.zone/prompt), ELF parsing, memory allocators, concurrency, UTF-8, globbing, and a whole lot more!
- `sp_io`, synchronous, bufferable, zero-copy IO
- File monitors, [beautiful, interactive CLIs](https://spader.zone/prompt), ELF parsing, memory allocators, concurrency, UTF-8, screaming fast globbing, and a whole lot more!

It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It can be used with virtually any 64-bit environment and toolchain:
It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It does not depend on libc. It can be used with virtually any environment and toolchain:
- Linux, macOS, Windows
- x86, ARM, or WASM
- gcc, clang, MSVC, mingw, zig cc, tcc, cosmocc
Expand All @@ -22,7 +22,7 @@ It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It can
#include "sp.h"
```

`sp.h` can be also be compiled as a traditional shared or static library.
`sp.h` can be also be compiled as a traditional shared or static library. `sp.h` makes no assumptions about its place in your code. You can use any small piece of it as a standalone utility, or you can use it as the foundation for almost any program. Give it a try!

## example: `ls`
Here's a minimal `ls` in 30 lines of code.
Expand Down Expand Up @@ -58,9 +58,10 @@ s32 main(s32 num_args, const c8** args) {
}
```
A few modules showcased in this example:
- `sp_mem_t` is an allocator; everything that allocates takes one. In the example, we use the default heap allocator.
- `sp_str_t` is a non-null-terminated string which trivially gives us views, substrings, and many path operations
- `sp_fs` is more or less equivalent to `std::fs` in C++, but in plain C and implemented against the lowest level APIs
- `sp_da` is a `std::vector` equivalent which can hold arbitrary types, does not need initialization, and is stored as `T*`
- `sp_da` is a `std::vector` equivalent which can hold arbitrary types and is stored as and usable as a plain `T*`
- `sp_fmt` implements modern format strings (like Zig, or Rust) whose arguments are type-safe

# modules
Expand Down Expand Up @@ -107,84 +108,17 @@ These are available in `sp/*.h` as separate headers, for various reasons.[^3]
| `sp_prompt` | Very beautiful [`clack`](https://github.com/bombshell-dev/clack)-inspired interactive prompts for CLIs | |

# principles
I wrote about some of the core design [here](https://spader.zone/sp/). The short version:
- Prefer the lowest level interface to the OS by default
- Ergonomics are the most important thing and by a lot
- Errors are propagated up the call stack
- Keep macros sane, and never generate code with an external tool
- Null terminated strings are the devil's work and are to be shunned
- A little Assembly never hurt anyone

# known issues
`sp.h` is a library that grows with my understanding of systems programming. That means that some of the code is naive, underspecified, or just bad. Everything that exists is tested extremely thoroughly.
Please note that `sp.h` is in alpha. I [use](https://github.com/tspader/spn) [it](https://github.com/tspader/space), [a](https://github.com/tspader/mbench) [lot](https://github.com/tspader/tomlc17), and there are about a thousand tests, but...it's still in alpha. The core API shape is done, but there will likely be breakage around the edges. There are also several POSIX-isms which have clung around, and `sp_io` was recently finished.

| module | problem | platform |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `sp_ps` | Implemented with `pthread` instead of `fork` + `exec` | Linux |
| `sp_ht` | Keys, by default, are simply `memcmp`'d for equality. If your key is a struct which the compiler pads, it is silently wrong. | |
| `sp_io` | Writes are objectively worse than libc, because we don't use `writev` to batch when we know we want to do more than one write (e.g. "flush the buffer and then immediately write the requested data") | |

# more examples
[Source code is often the best documentation](https://github.com/tspader/sp/tree/main/example), but here are a few more.

## wc
Here's a minimal version of a word frequency counter. It uses a few very handy and common functions:
- We use `sp_fs_join_path()` to find the target's absolute path
- Then, read it in one go with `sp_io_read_file()` (a thin wrapper over `sp_io_reader_t`)
- Split the content into lines, and then words. This is all zero copy; `lines` and `words` contain *views* into the content.
- A `sp_str_ht(u32)` (`str` -> `u32`) keeps the counts. `sp_str_ht_for_kv()` lets us iterate with a strongly typed (!) iterator
```c
#define SP_IMPLEMENTATION
#include "sp.h"

s32 main(s32 num_args, const c8** args) {
if (num_args < 2) {
sp_log("usage: wc {.fg cyan}", sp_fmt_cstr("$file"));
return 1;
}

sp_str_t path = sp_fs_join_path(sp_fs_get_cwd(), sp_str_view(args[1]));
sp_str_t content = sp_zero;
sp_io_read_file(path, &content);

sp_str_ht(u32) counts = sp_zero;
sp_da(sp_str_t) lines = sp_str_split_c8(content, '\n');
sp_da_for(lines, i) {
sp_da(sp_str_t) words = sp_str_split_c8(lines[i], ' ');

sp_da_for(words, j) {
u32* count = sp_str_ht_get(counts, words[j]);
if (count) {
*count = *count + 1;
} else {
sp_str_ht_insert(counts, words[j], 1);
}
}
}

sp_str_ht_for_kv(counts, it) {
sp_log("{} {}", sp_fmt_uint(*it.val), sp_fmt_str(*it.key));
}
return 0;
}
```

## dynamic array
```c
sp_da(u32) years = sp_zero;
sp_da_push(years, 1969);
sp_da_push(years, 1972);
sp_da_for(years, it) {
sp_log("{}", sp_fmt_uint(years[it]));
}
```

## hash table
```c
sp_cstr_ht(s32) ht = sp_zero;
sp_cstr_ht_insert(ht, "veneta", 72);
s32* veneta = sp_cstr_ht_get(ht, "veneta");
sp_log("the best dead show was in 19{}", sp_fmt_int(*veneta));
```
Thankfully, since the library is a single file, it's very easy for you or an LLM to diff two copies of the library and make any changes needed. More, since the library is *not* build on decades of cruft, there are very few layers between your code and the syscalls it boils down to. Nevertheless, you should feel comfortable reading the library's source code if you plan to use it seriously.

# development
Install any C compiler, and then:
Expand Down
84 changes: 70 additions & 14 deletions sp.h
Original file line number Diff line number Diff line change
Expand Up @@ -2514,6 +2514,11 @@ typedef enum {
SP_OS_FOLLOW_SYMLINK,
} sp_os_follow_symlink_t;

typedef enum {
SP_FS_PATH_POSIX,
SP_FS_PATH_WINDOWS,
} sp_fs_path_kind_t;

typedef struct {
sp_str_t path;
sp_str_t name;
Expand Down Expand Up @@ -2544,7 +2549,11 @@ SP_API sp_str_t sp_fs_trim_path(sp_str_t path);
SP_API sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path);
SP_API sp_str_t sp_fs_get_ext(sp_str_t path);
SP_API sp_str_t sp_fs_get_stem(sp_str_t path);
SP_API bool sp_fs_is_sep(c8 c);
SP_API bool sp_fs_is_root(sp_str_t path);
SP_API bool sp_fs_is_absolute(sp_str_t path);
SP_API bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind);
SP_API bool sp_fs_is_absolute_w(sp_wide_str_t path);
SP_API bool sp_fs_is_glob(sp_str_t path);
SP_API sp_str_t sp_fs_join_path(sp_mem_t mem, sp_str_t a, sp_str_t b);
SP_API sp_str_t sp_fs_replace_ext(sp_mem_t mem, sp_str_t path, sp_str_t ext);
Expand Down Expand Up @@ -2739,7 +2748,8 @@ SP_TYPEDEF_FN(void, sp_os_signal_handler_t, sp_os_signal_t signal, void* userdat

SP_API void sp_sleep_ns(u64 ns);
SP_API void sp_sleep_ms(f64 ms);
SP_API sp_os_kind_t sp_os_get_kind();
SP_API sp_os_kind_t sp_os_get_kind();
SP_API sp_fs_path_kind_t sp_os_get_path_kind();
SP_API sp_str_t sp_os_get_name();
SP_API sp_str_t sp_os_get_executable_ext();
SP_API sp_str_t sp_os_lib_kind_to_extension(sp_os_lib_kind_t kind);
Expand Down Expand Up @@ -4399,14 +4409,6 @@ SP_PRIVATE u8* sp_nt_process_params(void) {

#define SP_NT_OBJECT_NAME_INFORMATION 1

SP_PRIVATE bool sp_sys_is_absolute_wtf16(const u16* p, u32 len) {
if (len < 1) return false;
if (p[0] == '\\' || p[0] == '/') return true;
if (len < 2 || p[1] != ':') return false;
if (len < 3) return false;
return p[2] == '\\' || p[2] == '/';
}

SP_PRIVATE u32 sp_sys_normalize_relative_wtf16(u16* p, u32 len, bool* saw_dotdot) {
u32 w = 0;
u32 r = 0;
Expand Down Expand Up @@ -4473,7 +4475,7 @@ SP_PRIVATE sp_nt_status_t sp_sys_nt_target(sp_sys_fd_t root_fd, sp_str_t utf8, u
sp_wide_str_t wpath = sp_wtf8_to_wtf16(sp_mem_fixed_as_allocator(&fixed), utf8);
if (!wpath.data) return SP_NT_STATUS_OBJECT_NAME_INVALID;

if (sp_sys_is_absolute_wtf16(wpath.data, wpath.len)) {
if (sp_fs_is_absolute_w(wpath)) {
sp_nt_unicode_string_t nt = sp_zero;
u16* file_part = SP_NULLPTR;
sp_nt_status_t status = SP_NT(RtlDosPathNameToNtPathName_U_WithStatus)(wpath.data, &nt, &file_part, SP_NULLPTR);
Expand Down Expand Up @@ -9717,6 +9719,10 @@ sp_os_kind_t sp_os_get_kind() {
return SP_OS_WIN32;
}

sp_fs_path_kind_t sp_os_get_path_kind() {
return SP_FS_PATH_WINDOWS;
}

sp_str_t sp_os_get_executable_ext() {
return sp_str_lit("exe");
}
Expand All @@ -9740,6 +9746,10 @@ sp_os_kind_t sp_os_get_kind() {
return SP_OS_MACOS;
}

sp_fs_path_kind_t sp_os_get_path_kind() {
return SP_FS_PATH_POSIX;
}

sp_str_t sp_os_get_executable_ext() {
return sp_str_lit("");
}
Expand All @@ -9763,6 +9773,10 @@ sp_os_kind_t sp_os_get_kind() {
return SP_OS_LINUX;
}

sp_fs_path_kind_t sp_os_get_path_kind() {
return SP_FS_PATH_POSIX;
}

sp_str_t sp_os_get_executable_ext() {
return sp_str_lit("");
}
Expand Down Expand Up @@ -9796,6 +9810,16 @@ sp_os_kind_t sp_os_get_kind() {
SP_UNREACHABLE_RETURN(SP_OS_LINUX);
}

sp_fs_path_kind_t sp_os_get_path_kind() {
switch (sp_os_get_kind()) {
case SP_OS_LINUX: return SP_FS_PATH_POSIX;
case SP_OS_MACOS: return SP_FS_PATH_POSIX;
case SP_OS_WIN32: return SP_FS_PATH_WINDOWS;
}

SP_UNREACHABLE_RETURN(SP_FS_PATH_POSIX);
}

sp_str_t sp_os_get_executable_ext() {
switch (sp_os_get_kind()) {
case SP_OS_LINUX: return sp_str_lit("");
Expand All @@ -9820,6 +9844,12 @@ sp_str_t sp_os_lib_to_file_name(sp_mem_t mem, sp_str_t lib_name, sp_os_lib_kind_
}
#endif

#if defined(SP_WASM)
sp_fs_path_kind_t sp_os_get_path_kind() {
return SP_FS_PATH_POSIX;
}
#endif

////////////
// SIGNAL //
////////////
Expand Down Expand Up @@ -14926,11 +14956,37 @@ sp_str_t sp_fs_get_stem(sp_str_t path) {
return stem;
}

bool sp_fs_is_sep(c8 c) {
return c == '/' || c == '\\';
}

bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind) {
if (path.len == 0) return false;

if (sp_fs_is_sep(path.data[0])) {
return true;
}

if (kind != SP_FS_PATH_WINDOWS) return false;

return (path.len >= 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2]));
}

bool sp_fs_is_absolute(sp_str_t path) {
return sp_fs_is_absolute_for(path, sp_os_get_path_kind());
}

bool sp_fs_is_root(sp_str_t path) {
if (path.len == 0) return true;
if (path.len == 1 && path.data[0] == '/') return true;
if (path.len == 2 && path.data[1] == ':') return true;
if (path.len == 3 && path.data[1] == ':' && (path.data[2] == '/' || path.data[2] == '\\')) return true;
if (path.len == 1 && sp_fs_is_sep(path.data[0])) return true;
if (path.len == 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2])) return true;
return false;
}

bool sp_fs_is_absolute_w(sp_wide_str_t path) {
if (path.len == 0) return false;
if (path.data[0] == '/' || path.data[0] == '\\') return true;
if (path.len >= 3 && path.data[1] == ':' &&
(path.data[2] == '/' || path.data[2] == '\\')) return true;
return false;
}

Expand Down
1 change: 1 addition & 0 deletions test/fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ SP_TEST_MAIN()
#include "fs/get_stem.c"
#include "fs/join_path.c"
#include "fs/replace_ext.c"
#include "fs/is_absolute.c"
#include "fs/is_root.c"
#include "fs/is_glob.c"
#include "fs/canonicalize_path.c"
Expand Down
20 changes: 3 additions & 17 deletions test/fs/canonicalize_path.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,24 +288,10 @@ UTEST_F(fs, canon_idempotent) {
SP_EXPECT_STR_EQ(first, second);
}

UTEST_F(fs, canon_exe_idempotent) {
UTEST_F(fs, canon_dot_resolves_to_cwd) {
SKIP_ON_WASM()
sp_mem_t a = ut.file_manager.mem;
sp_str_t exe = sp_fs_get_exe_path(a);
sp_str_t canonical = sp_fs_canonicalize_path(a, exe);
SP_EXPECT_STR_EQ(canonical, exe);
}

UTEST_F(fs, canon_cwd_matches_dot) {
SKIP_ON_WASM()
sp_mem_t a = ut.file_manager.mem;
sp_str_t old_cwd = sp_fs_get_cwd(a);
sp_str_t sandbox = sp_test_file_path(&ut.file_manager, sp_str_lit("canon_cwd"));
sp_fs_create_dir(sandbox);

ASSERT_EQ(sp_sys_chdir_s(sandbox), 0);
sp_str_t cwd = sp_fs_get_cwd(a);
sp_str_t canonical_dot = sp_fs_canonicalize_path(a, sp_str_lit("."));
SP_EXPECT_STR_EQ(cwd, canonical_dot);
ASSERT_EQ(sp_sys_chdir_s(old_cwd), 0);
sp_str_t dot = sp_fs_canonicalize_path(a, sp_str_lit("."));
SP_EXPECT_STR_EQ(cwd, dot);
}
Loading
Loading