Compare commits

..

42 Commits

Author SHA1 Message Date
dc839e098c media: Update to zig 0.16.0, harden tests and add test build step 2026-05-13 00:11:38 +02:00
380145a986 vecmath: Update to zig 0.16.0, harden tests and add test build step 2026-05-12 23:33:33 +02:00
0cce9d9bce Add "zig-pkg" to .gitignore (for zig 0.16.0) 2026-05-12 23:11:34 +02:00
572f4be896 myid port (wip) 2026-05-12 23:11:15 +02:00
8d45a93e6e bad grammer 2026-05-12 23:10:12 +02:00
09266e678f README for the people 2026-03-13 14:44:41 +01:00
cbf0d6a9da Massive logging, improve connection closing/timeout handling 2026-03-13 02:47:36 +01:00
712e214f61 web: fix compiler errors 2026-03-12 03:07:05 +01:00
fe4a585b6b web: unnecessary refactor before compilation 2026-03-12 02:47:54 +01:00
9a4932e629 web: Great cleanup WIP 2026-03-09 16:59:29 +01:00
6315589fa1 web: SSL but bad 2026-03-08 23:37:22 +01:00
1f07cc38ba web: The beginnings of TLS 2026-03-08 22:08:25 +01:00
e09a00a4ba web: fix compile errors and critical runtime errors 2026-03-08 15:48:20 +01:00
738ba5bd37 web: before compile & fix marathon 2026-03-08 14:54:09 +01:00
66d49ea8d5 web: some stuff 2026-03-07 21:08:22 +01:00
f02ece22fa web: Secondary utils and helpers 2026-03-06 13:17:27 +01:00
2e97aef842 media: png chunk structs and decoders 2026-02-15 23:51:21 +01:00
9e7955495f Remove sciter and TCC from code workspace 2026-02-09 19:03:59 +01:00
ef15201f21 media: Format description struct, stubs for more formats 2026-02-09 19:02:02 +01:00
85f4957661 media: fix compilation errors, HDR loading; vecmath: ColorHdr 2026-02-06 23:26:11 +01:00
2756957f9b media: add stb_image 2026-02-06 01:42:42 +01:00
4f2aad8065 Remove sciter and tcc packages 2026-02-05 19:33:18 +01:00
f2b7817cac cjit: Fix some compilation errors 2026-01-27 14:56:39 +01:00
0a3d82a562 cjit: Some standard headers, builtins 2026-01-24 22:43:57 +01:00
6363bc3bd1 cjit update 2026-01-23 01:13:15 +01:00
868550703f cjit update 2026-01-22 22:30:58 +01:00
eb3c3814ec cjit update 2026-01-20 21:40:47 +01:00
fa42b20136 Tabs vs spaces 2026-01-19 14:16:44 +01:00
0b23707041 Dream of my own C compiler 2026-01-19 14:01:46 +01:00
b0deb958d2 Add TCC project (as a library) 2026-01-15 22:33:56 +01:00
d03910f6f0 X11 events 2026-01-09 20:27:09 +01:00
a6f67d3f0d X11 library (WIP) 2026-01-09 16:08:48 +01:00
9fb8ec9454 Add LLVM option to Sciter and then give up 2026-01-08 22:59:00 +01:00
99f8ae059c X11 API partial cleanup 2026-01-08 22:17:06 +01:00
1e8e14ed67 VS Code workspace setup 2026-01-08 17:34:53 +01:00
11e948d500 Sciter failed Linux update 2026-01-08 17:29:39 +01:00
31651dc96a Integer lerp and unlerp 2026-01-07 13:54:32 +01:00
34f0b1fb89 Vector int-float conversion methods 2026-01-06 21:14:12 +01:00
7963813034 Nothing 2026-01-06 21:13:16 +01:00
49cf6e4237 Move test 2026-01-06 14:41:15 +01:00
6c9a786926 Fix Quaternion → Matrix conversion 2026-01-05 23:57:54 +01:00
a17a39a9a4 Add some format methods 2026-01-05 23:57:17 +01:00
118 changed files with 57099 additions and 1787 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1 @@
*.dll filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.zig-cache
zig-out
zig-pkg

View File

@@ -1,5 +1,8 @@
{
"files.exclude":{
"files.exclude": {
"**/.zig-cache": true,
},
"files.associations": {
"**/*.{c,h}": "c",
},
}

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# Building my own castle
I'm building my own castle in Zig ⚡.
This is a collection of experimental, recreational or actually useful libraries,
which I may or may not use as a part of some other projects. Most of them are
work in progress and might remain so indefinitely.
## cjit
JIT compiler for the C language inspired by Fabrice Bellard's Tiny C Compiler
(TCC). Meant to compile x86_64 and aarch64 for Windows and Linux straight to
virtual memory, without the ability to output an executable file or library. C
standard library is not fully supported, only some includes and builtins (see
[src/includes](packages/cjit/src/includes)).
It's in very early stage and cannot be used as of now. Currently, parts of the
tokenizer, x86_64 emit code and relocation logic are implemented. Aarch64 would
come after x86_64 is fully implemented and actually works.
You can see an example of how this library is supposed to be used in
[test/root.zig](packages/cjit/test/root.zig).
## js
Zig bindings for Fabrice Bellard's QuickJS. Original C sources are included and
compiled with Zig's build system. The bindings are not complete, but usable.
## media
Set of utilities for decoding and encoding media files. As of now, no purely Zig
decoder/encoder exists, which would be the goal. There is fairly comprehensive
PNG chunk decoder, though.
Bindings for stb_image.h from Sean T. Barrett's single-file C libraries are
included. They can be used to decode PNG, JPEG and HDR images and support Zig's
allocator interface in a thread-safe manner.
This package is actually used by my other project, codenamed
[voxel-game](/:root/renati/voxel-game).
## myid
Remnants of a project meant to provide a simpler alternative to OpenID Connect
(OIDC). It contains an HTTP server, which has since been improved and resides
in *web* package.
The ideas behind *myid* were developed further in another project I created
using TypeScript and Bun as a runtime. I might backport them back to Zig when I
find the time and motivation, which would be implemented on top of the HTTP
server from the *web* package.
## vecmath
Vector math library with support for vectors, matrices and ×8 SIMD operations
(utilizing SOA layout).
This package is actually used by my other project, codenamed
[voxel-game](/:root/renati/voxel-game) and is probably the most complete package
in this repository.
## web
An HTTP server library and collection of other utilities commonly associated
with web technologies.
The HTTP server implements HTTP version 1.1 using worker threads and supports
SSL via OpenSSL library. The server is very low level and does not provide any
logic to handle proper HTTP semantics. It is up to the user of the HTTP server
library to respect (or not) all HTTP methods and headers. The server is designed
or Linux only and uses Linux syscalls directly in its implementation.
The other utilities include:
- HTTP/1.1 request parser, which is part of the HTTP server, but can be used
independently,
- OpenSSL bindings, which are used by the HTTP server, but can be used
independently (full mechanical C translation is provided and partial manual
Zig translation),
- UUID type and generators for v4, v5 and v7,
- Generic ID type, a wrapper around UUID (or anything that is 16 bytes long)
that can be instantiated for each distinct use of ID with a tag to help avoid
type confusion at compile time.
## x11
Zig bindings for Xlib (aka libX11), a C library implementing a client of the X
Window System protocol. The bindings are not complete.

44
packages/cjit/build.zig Normal file
View File

@@ -0,0 +1,44 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{
.whitelist = &.{
.{
.cpu_arch = .x86_64,
.os_tag = .windows,
.abi = .gnu,
},
.{
.cpu_arch = .x86_64,
.os_tag = .linux,
.abi = .gnu,
},
},
});
const optimize = b.standardOptimizeOption(.{});
const module = b.addModule("cjit", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
const module_internal_test = b.addTest(.{ .root_module = module });
const run_internal_test = b.addRunArtifact(module_internal_test);
const module_external_test = b.addTest(.{
.root_module = b.createModule(.{
.target = b.resolveTargetQuery(.{}),
.optimize = .Debug,
.root_source_file = b.path("test/root.zig"),
.imports = &.{
.{ .name = "cjit", .module = module },
},
}),
});
const run_external_test = b.addRunArtifact(module_external_test);
const step_test = b.step("test", "Run tests");
step_test.dependOn(&run_internal_test.step);
step_test.dependOn(&run_external_test.step);
}

View File

@@ -0,0 +1,11 @@
.{
.name = .cjit,
.version = "0.0.0",
.minimum_zig_version = "0.15.2",
.paths = .{
"src",
"build.zig",
"build.zig.zon",
},
.fingerprint = 0xaec0accc19243440,
}

View File

@@ -0,0 +1,154 @@
const std = @import("std");
const builtin = @import("builtin");
const Self = @This();
const tokens = @import("tokens.zig");
const types = @import("types.zig");
const Sections = @import("Sections.zig");
const StackValue = @import("StackValue.zig");
const Tokenizer = tokens.Tokenizer;
const Type = types.Type;
pub const virtual_stack_size = 256;
allocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
symbols_needed: std.StringHashMapUnmanaged(ExternSymbol) = .{},
symbols_provided: std.StringHashMapUnmanaged(InternSymbol) = .{},
symbols_located: std.StringHashMapUnmanaged(LocatedSymbol) = .{},
relocation_table: std.ArrayList(Relocation) = .{},
/// Bounded, preallocated with the capacity of `virtual_stack_size`.
virtual_stack: std.ArrayList(StackValue),
tokenizer: Tokenizer,
sections: Sections = .{},
pub const ExternSymbol = struct {
name: []const u8,
c_type: Type,
ptr: ?*anyopaque,
};
pub const InternSymbol = struct {
name: []const u8,
public: bool,
c_type: Type,
location: Location,
};
pub const Location = union(enum) {
text: usize,
data: usize,
rodata: usize,
};
pub const LocatedSymbol = struct {
name: []const u8,
public: bool,
c_type: Type,
ptr: ?*anyopaque,
};
pub const Relocation = struct {
addr: usize,
location: Location,
type: RelocationType,
};
pub const RelocationType = enum {
global_offset_table,
rip_disp32,
};
pub fn init(allocator: std.mem.Allocator) !Self {
var arena: std.heap.ArenaAllocator = .init(allocator);
const arena_allocator = arena.allocator();
errdefer arena.deinit();
const virtual_stack_buffer = try arena_allocator.alloc(StackValue, virtual_stack_size);
const tokenizer = try Tokenizer.init(arena_allocator);
return .{
.allocator = allocator,
.arena = arena,
.virtual_stack = .initBuffer(virtual_stack_buffer),
.tokenizer = tokenizer,
};
}
pub fn deinit(self: *Self) void {
self.symbols_needed.deinit(self.allocator);
self.symbols_provided.deinit(self.allocator);
self.symbols_located.deinit(self.allocator);
self.relocation_table.deinit(self.allocator);
self.arena.deinit();
// TODO deinit sections (stage-depentent)
self.* = undefined;
}
pub fn compile(self: *Self, filename: []const u8, code: []const u8) !void {
self.tokenizer.setSource(filename, code);
while (try self.tokenizer.nextToken(self.arena.allocator())) |token| {
_ = token;
std.debug.panic("Not implemented", .{});
}
}
pub fn setSymbol(self: *Self, comptime T: type, name: []const u8, ptr: *const T) !void {
const symbol = self.symbols_needed.getPtr(name) orelse return error.SymbolNotFound;
if (!types.isCompatible(T, symbol.c_type)) return error.IncompatibleType;
// TODO Figure out const-correctness
symbol.ptr = @ptrCast(@constCast(ptr));
}
pub fn link(self: *Self) !void {
try self.sections.relocateSections(self.allocator);
const text_base = @intFromPtr(self.sections.text.data.items.ptr);
const data_base = @intFromPtr(self.sections.data.data.items.ptr);
const rodata_base = @intFromPtr(self.sections.rodata.data.items.ptr);
for (self.relocation_table.items) |relocation| {
const target_addr = switch (relocation.location) {
.text => |offset| text_base + offset,
.data => |offset| data_base + offset,
.rodata => |offset| rodata_base + offset,
};
switch (relocation.type) {
.global_offset_table => {
std.mem.writeInt(
usize,
self.sections.rodata.data.items[relocation.addr..][0..@sizeOf(usize)],
target_addr,
builtin.cpu.arch.endian(),
);
},
.rip_disp32 => {
const rip = text_base + relocation.addr + 4;
const disp64: isize = @bitCast(rip -% target_addr);
const disp32 = std.math.cast(i32, disp64) orelse return error.RelocationError;
std.mem.writeInt(
i32,
self.sections.text.data.items[relocation.addr..][0..4],
disp32,
builtin.cpu.arch.endian(),
);
},
}
}
self.relocation_table.clearAndFree(self.allocator);
try self.sections.protectSections();
}
pub fn getSymbol(self: *const Self, comptime T: type, name: []const u8) ?*const T {
const symbol = self.symbols_located.get(name) orelse return null;
return @ptrCast(@alignCast(symbol.ptr));
}
fn create(self: *Self, comptime T: type) error{OutOfMemory}!*T {
return self.arena.allocator().create(T);
}

View File

@@ -0,0 +1,185 @@
const std = @import("std");
const builtin = @import("builtin");
const Self = @This();
text: Section = .{ .protection = .executable },
data: Section = .{ .protection = .read_write },
rodata: Section = .{ .protection = .read_only },
pub const Section = struct {
data: std.ArrayList(u8) = .{},
protection: Protection,
pub const Protection = enum {
executable,
read_only,
read_write,
};
pub fn writeValue(self: *Section, value: anytype, allocator: std.mem.Allocator) !void {
const T = @TypeOf(value);
std.debug.assert(std.meta.hasUniqueRepresentation(T));
const bytes = std.mem.asBytes(&value);
try self.writeBytes(bytes, allocator);
}
pub fn writeByte(self: *Section, byte: u8, allocator: std.mem.Allocator) !void {
try self.data.append(allocator, byte);
}
pub fn writeBytes(self: *Section, data: []const u8, allocator: std.mem.Allocator) !void {
try self.data.appendSlice(allocator, data);
}
pub fn alignForward(self: *Section, alignment: u16, allocator: std.mem.Allocator) !void {
const ptr = self.data.items.len;
const ptr_aligned = std.mem.alignForward(usize, ptr, alignment);
const padding = ptr_aligned - ptr;
try self.data.appendNTimes(allocator, 0, padding);
}
pub fn pageCount(self: Section) usize {
const page_size = std.heap.pageSize();
const section_size = self.data.items.len;
return @divFloor(section_size + page_size - 1, page_size);
}
};
pub fn relocateSections(self: *Self, allocator: std.mem.Allocator) !void {
const page_size = std.heap.pageSize();
const text_pages = self.text.pageCount();
const text_bytes = text_pages * page_size;
const data_pages = self.data.pageCount();
const data_bytes = data_pages * page_size;
const rodata_pages = self.rodata.pageCount();
const rodata_bytes = rodata_pages * page_size;
const total_pages = text_pages + data_pages + rodata_pages;
const total_bytes = text_bytes + data_bytes + rodata_bytes;
std.debug.assert(total_bytes == total_pages * page_size);
const ptr: [*]u8 = sw: switch (builtin.os.tag) {
.windows => {
const windows = std.os.windows;
const ntdll = windows.ntdll;
var base_addr: ?*anyopaque = null;
var size: windows.SIZE_T = total_bytes;
const status = ntdll.NtAllocateVirtualMemory(
windows.GetCurrentProcess(),
@ptrCast(&base_addr),
0,
&size,
windows.MEM_COMMIT | windows.MEM_RESERVE,
windows.PAGE_READWRITE,
);
if (status == .SUCCESS) {
break :sw @ptrCast(base_addr);
} else {
return error.OutOfMemory;
}
},
.linux => {
const linux = std.os.linux;
const rc = linux.mmap(
null,
total_bytes,
linux.PROT.READ | linux.PROT.WRITE,
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
const status: linux.E = .init(rc);
if (status == .SUCCESS) {
break :sw @ptrFromInt(rc);
} else {
return error.OutOfMemory;
}
},
else => @compileError("Operating system " ++ @tagName(builtin.os.tag) ++ " not supported"),
};
const text_slice = ptr[0..text_bytes];
const data_slice = ptr[text_bytes .. text_bytes + data_bytes];
const rodata_slice = ptr[text_bytes + data_bytes .. text_bytes + data_bytes + rodata_bytes];
@memcpy(text_slice[0..self.text.data.items.len], self.text.data.items);
@memcpy(data_slice[0..self.data.data.items.len], self.data.data.items);
@memcpy(rodata_slice[0..self.rodata.data.items.len], self.rodata.data.items);
self.text.data.clearAndFree(allocator);
self.data.data.clearAndFree(allocator);
self.rodata.data.clearAndFree(allocator);
self.text.data = .{ .items = text_slice, .capacity = text_bytes };
self.data.data = .{ .items = data_slice, .capacity = data_bytes };
self.rodata.data = .{ .items = rodata_slice, .capacity = rodata_bytes };
}
pub fn protectSections(self: *Self) !void {
const sections = [_]*Section{ &self.text, &self.data, &self.rodata };
for (sections) |section| {
switch (builtin.os.tag) {
.windows => {
const windows = std.os.windows;
const ntdll = windows.ntdll;
const protection = switch (section.protection) {
.executable => windows.PAGE_EXECUTE,
.read_only => windows.PAGE_READONLY,
.read_write => windows.PAGE_READWRITE,
};
var base_addr: ?*anyopaque = section.data.items.ptr;
var size: windows.SIZE_T = section.data.capacity;
var old_protection: u32 = undefined;
const status = ntdll.NtProtectVirtualMemory(
windows.GetCurrentProcess(),
&base_addr,
&size,
protection,
&old_protection,
);
if (status != .SUCCESS) {
return error.ProtectionError;
}
},
.linux => {
const linux = std.os.linux;
const protection: usize = switch (section.protection) {
.executable => linux.PROT.EXEC,
.read_only => linux.PROT.READ,
.read_write => linux.PROT.READ | linux.PROT.WRITE,
};
const rc = linux.mprotect(
section.data.items.ptr,
section.data.capacity,
protection,
);
const status: linux.E = .init(rc);
if (status != .SUCCESS) {
return error.ProtectionError;
}
},
else => @compileError("Operating system " ++ @tagName(builtin.os.tag) ++ " not supported"),
}
}
}
pub fn freeSections(self: *Self) void {
_ = self;
std.debug.panic("Not implemented", .{});
}

View File

@@ -0,0 +1,20 @@
const std = @import("std");
const Self = @This();
const types = @import("types.zig");
const x86_64 = @import("x86_64.zig");
const Register = x86_64.Register;
const Type = types.Type;
pub const Value = union(enum) {
register: Register,
constant: u64,
symbol: []const u8,
/// Displacement in bytes from current value of base pointer register.
stack: i32,
cpu_flags: void,
};
c_type: Type,
value: Value,

View File

@@ -0,0 +1,31 @@
#pragma once
#define abs(value) __builtin_abs(value)
#define byteswap(value) __builtin_byteswap(value)
#define ceil(value) __builtin_ceil(value)
#define clz(value) __builtin_clz(value)
#define containerof(ptr, type, member) __builtin_containerof(ptr, type, member)
#define cos(value) __builtin_cos(value)
#define ctz(value) __builtin_ctz(value)
#define embedfile(path) __builtin_embedfile(path)
#define exp(value) __builtin_exp(value)
#define exp2(value) __builtin_exp2(value)
#define floor(value) __builtin_floor(value)
#define frameaddress() __builtin_frameaddress()
#define log(value) __builtin_log(value)
#define log10(value) __builtin_log10(value)
#define log2(value) __builtin_log2(value)
#define max(...) __builtin_max(__VA_ARGS__)
#define memcpy(dest, src, count) __builtin_memcpy(dest, src, count)
#define memmove(dest, src, count) __builtin_memmove(dest, src, count)
#define memset(value) __builtin_memset(value)
#define min(...) __builtin_min(__VA_ARGS__)
#define popcount(value) __builtin_popcount(value)
#define returnaddress() __builtin_returnaddress()
#define round(value) __builtin_round(value)
#define sin(value) __builtin_sin(value)
#define sqrt(value) __builtin_sqrt(value)
#define tan(value) __builtin_tan(value)
#define trunc(value) __builtin_trunc(value)
#define typename(type) __builtin_typename(type)
#define typeof(...) __builtin_typeof(__VA_ARGS__)

View File

@@ -0,0 +1,7 @@
#pragma once
#define alignas _Alignas
#define alignof _Alignof
#define __alignas_is_defined 1
#define __alignof_is_defined 1

View File

@@ -0,0 +1,7 @@
#pragma once
#define bool _Bool
#define true 1
#define false 0
#define __bool_true_false_are_defined 1

View File

@@ -0,0 +1,9 @@
#pragma once
typedef long ptrdiff_t;
typedef long max_align_t;
typedef unsigned long size_t;
#define NULL ((void *)0)
#define offsetof(type, member) __builtin_offsetof(type, member)

View File

@@ -0,0 +1,118 @@
#pragma once
typedef signed char int8_t;
typedef short int16_t;
typedef int int32_t;
typedef long int64_t;
typedef int8_t int_fast8_t;
typedef int16_t int_fast16_t;
typedef int32_t int_fast32_t;
typedef int64_t int_fast64_t;
typedef int8_t int_least8_t;
typedef int16_t int_least16_t;
typedef int32_t int_least32_t;
typedef int64_t int_least64_t;
typedef int64_t intmax_t;
typedef int64_t intptr_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long uint64_t;
typedef uint8_t uint_fast8_t;
typedef uint16_t uint_fast16_t;
typedef uint32_t uint_fast32_t;
typedef uint64_t uint_fast64_t;
typedef uint8_t uint_least8_t;
typedef uint16_t uint_least16_t;
typedef uint32_t uint_least32_t;
typedef uint64_t uint_least64_t;
typedef uint64_t uintmax_t;
typedef uint64_t uintptr_t;
#define INT8_MIN (-0x80)
#define INT16_MIN (-0x8000)
#define INT32_MIN (-0x80000000)
#define INT64_MIN (-0x8000000000000000L)
#define INT_FAST8_MIN INT8_MIN
#define INT_FAST16_MIN INT16_MIN
#define INT_FAST32_MIN INT32_MIN
#define INT_FAST64_MIN INT64_MIN
#define INT_LEAST8_MIN INT8_MIN
#define INT_LEAST16_MIN INT16_MIN
#define INT_LEAST32_MIN INT32_MIN
#define INT_LEAST64_MIN INT64_MIN
#define INTPTR_MIN INT64_MIN
#define INTMAX_MIN INT64_MIN
#define INT8_MAX (0x7F)
#define INT16_MAX (0x7FFF)
#define INT32_MAX (0x7FFFFFFF)
#define INT64_MAX (0x7FFFFFFFFFFFFFFFL)
#define INT_FAST8_MAX INT8_MAX
#define INT_FAST16_MAX INT16_MAX
#define INT_FAST32_MAX INT32_MAX
#define INT_FAST64_MAX INT64_MAX
#define INT_LEAST8_MAX INT8_MAX
#define INT_LEAST16_MAX INT16_MAX
#define INT_LEAST32_MAX INT32_MAX
#define INT_LEAST64_MAX INT64_MAX
#define INTPTR_MAX INT64_MAX
#define INTMAX_MAX INT64_MAX
#define UINT8_MAX (0xFF)
#define UINT16_MAX (0xFFFF)
#define UINT32_MAX (0xFFFFFFFFU)
#define UINT64_MAX (0xFFFFFFFFFFFFFFFFUL)
#define UINT_FAST8_MAX UINT8_MAX
#define UINT_FAST16_MAX UINT16_MAX
#define UINT_FAST32_MAX UINT32_MAX
#define UINT_FAST64_MAX UINT64_MAX
#define UINT_LEAST8_MAX UINT8_MAX
#define UINT_LEAST16_MAX UINT16_MAX
#define UINT_LEAST32_MAX UINT32_MAX
#define UINT_LEAST64_MAX UINT64_MAX
#define UINTPTR_MAX UINT64_MAX
#define UINTMAX_MAX UINT64_MAX
#define INT8_C(value) value
#define INT16_C(value) value
#define INT32_C(value) value
#define INT64_C(value) value ## L
#define INTMAX_C(value) value ## L
#define UINT8_C(value) value
#define UINT16_C(value) value
#define UINT32_C(value) value ## U
#define UINT64_C(value) value ## UL
#define UINTMAX_C(value) value ## UL
#define PTRDIFF_MIN (-0x8000000000000000L)
#define PTRDIFF_MAX (0x7FFFFFFFFFFFFFFFL)
#define SIZE_MAX (0xFFFFFFFFFFFFFFFFUL)
#define WINT_MIN (-0x80000000)
#define WINT_MAX (0x7FFFFFFF)
#define WCHAR_MIN (0)
#define WCHAR_MAX (0x10FFFF)
#endif

View File

@@ -0,0 +1,3 @@
#pragma once
#define noreturn _Noreturn

View File

@@ -0,0 +1,19 @@
const std = @import("std");
const builtin = @import("builtin");
pub const Runtime = @import("Runtime.zig");
pub const Sections = @import("Sections.zig");
pub const StackValue = @import("StackValue.zig");
pub const tokens = @import("tokens.zig");
pub const types = @import("types.zig");
pub const x86_64 = @import("x86_64.zig");
pub const call: std.builtin.CallingConvention = switch (builtin.cpu.arch) {
.aarch64 => .{ .aarch64_aapcs = .{} },
.x86_64 => .{ .x86_64_sysv = .{} },
else => @compileError("Architecture " ++ @tagName(builtin.cpu.arch) ++ " not supported"),
};
test {
std.testing.refAllDeclsRecursive(@This());
}

View File

@@ -0,0 +1,7 @@
pub const Builtin = @import("tokens/Builtin.zig").Builtin;
pub const Constant = @import("tokens/Constant.zig").Constant;
pub const Keyword = @import("tokens/Keyword.zig").Keyword;
pub const Punctuator = @import("tokens/Punctuator.zig").Punctuator;
pub const Token = @import("tokens/Token.zig").Token;
pub const Tokenizer = @import("tokens/Tokenizer.zig");
pub const Utf8Iterator = @import("tokens/Utf8Iterator.zig");

View File

@@ -0,0 +1,156 @@
const std = @import("std");
pub const Builtin = enum {
/// Usage: `__builtin_abs(value)`
///
/// `value` can be any integer or real type. Equivalent to
/// `value < 0 ? -value : value`. Noop for unsigned integer types.
__builtin_abs,
/// Usage: `__builtin_byteswap(value)`
///
/// `value` can be any integer type.
__builtin_byteswap,
/// Usage: `__builtin_ceil(value)`
///
/// `value` can be any real type.
__builtin_ceil,
/// Usage: `__builtin_clz(value)`
///
/// Count leading zeroes. `value` can be any integer type. The return type
/// is `int`.
__builtin_clz,
/// Usage `__builtin_containerof(ptr, type, member)`
///
/// `ptr` must be an pointer to a struct or a union. `type` must be a type.
/// `member` must be an identifier. Given `ptr` is a pointer to a given
/// member of `type`, returns a pointer to the entire container.
__builtin_containerof,
/// Usage: `__builtin_cos(value)`
///
/// `value` can be any real type.
__builtin_cos,
/// Usage: `__builtin_ctz(value)`
///
/// Count trailing zeroes. `value` can be any integer type. The return type
/// is `int`.
__builtin_ctz,
/// Usage: `__builtin_embedfile(path)`
///
/// `path` must be a string literal. The return type is `const char *`. The
/// data is null-terminated.
__builtin_embedfile,
/// Usage `__builtin_exp(value)`
///
/// `value` can be any real type. Calculates e^value.
__builtin_exp,
/// Usage `__builtin_exp2(value)`
///
/// `value` can be any real type. Calculates 2^value.
__builtin_exp2,
/// Usage: `__bultin_floor(value)`
///
/// `value` can be any real type.
__builtin_floor,
/// Usage: `__builtin_frameaddress()`
///
/// Returns the value of base pointer. The return type is equivalent to
/// `uintptr_t`.
__builtin_frameaddress,
/// Usage: `__builtin_log(value)`
///
/// `value` can be any real type. Calculates natural logarithm (base e).
__builtin_log,
/// Usage: `__builtin_log10(value)`
///
/// `value` can be any real type. Calculates base 10 logarithm.
__builtin_log10,
/// Usage: `__builtin_log2(value)`
///
/// `value` can be any real type. Calculates base 2 logarithm.
__builtin_log2,
/// Usage: `__builtin_max(...)`
///
/// The arguments can be any integer or real types. NaN values are ignored.
__builtin_max,
/// Usage: `__builtin_memcpy(dest, src, count)`
///
/// `dest` and `src` must be pointers. The pointers are reinterpreted as
/// pointers to `char`. `count` is coerced to the equivalent of `size_t`.
/// `dest` must be a non-const pointer. The regions must not overlap.
__builtin_memcpy,
/// Usage: `__builtin_memmove(dest, src, count)`
///
/// `dest` and `src` must be pointers. The pointers are reinterpreted as
/// pointers to `char`. `count` is coerced to the equivalent of `size_t`.
/// `dest` must be a non-const pointer. The regions may overlap.
__builtin_memmove,
/// Usage: `__builtin_memset(dest, ch, count)`
///
/// `dest` must be a pointer. The pointer is reinterpreted as pointer to
/// `char`. `ch` is cast to `unsigned char`. `dest` must be a non-const
// pointer.
__builtin_memset,
/// Usage: `__builtin_min(...)`
///
/// The arguments can be any integer or real types. NaN values are ignored.
__builtin_min,
/// Usage: `__builtin_offsetof(type, member)`
///
/// `type` must be a type. `member` must be an identifier.
__builtin_offsetof,
/// Usage: `__builtin_popcount(value)`
///
/// `value` can be any integer type. The return type is `int`.
__builtin_popcount,
/// Usage: `__builtin_returnaddress()`
///
/// Returns the address of the instruction to run after current function
/// returns. The return type is equivalent to `uintptr_t`.
__builtin_returnaddress,
/// Usage: `__builtin_round(value)`
///
/// `value` can be any real type.
__builtin_round,
/// Usage: `__builtin_sin(value)`
///
/// `value` can be any real type.
__builtin_sin,
/// Usage: `__builtin_sqrt(value)`
///
/// `value` can be any real type.
__builtin_sqrt,
/// Usage: `__builtin_tan(value)`
///
/// `value` can be any real type.
__builtin_tan,
/// Usage: `__builtin_trunc(value)`
///
/// `value` can be any real type.
__builtin_trunc,
/// Usage: `__builtin_typename(type)`
///
/// `type` must be a type. The return type is `const char *`.
__builtin_typename,
/// Usage: `__builtin_typeof(...)`
__builtin_typeof,
pub const map: std.StaticStringMap(Builtin) = blk: {
const fields = @typeInfo(Builtin).@"enum".fields;
var kvs_list: [fields.len]struct { []const u8, Builtin } = undefined;
for (fields, 0..) |field, i| {
kvs_list[i] = .{ field.name, @field(Builtin, field.name) };
}
break :blk .initComptime(kvs_list);
};
pub fn isBuiltin(identifier: []const u8) ?Builtin {
if (std.mem.startsWith(u8, identifier, "__builtin_")) {
@branchHint(.unlikely);
return map.get(identifier);
} else {
return null;
}
}
};

View File

@@ -0,0 +1,14 @@
const std = @import("std");
pub const Constant = union(enum) {
int: i32,
long: i64,
long_long: i64,
unsigned_int: u32,
unsigned_long: u64,
unsigned_long_long: u64,
float: f32,
double: f64,
character: u8,
wide_character: i32,
};

View File

@@ -0,0 +1,63 @@
const std = @import("std");
pub const Keyword = enum {
_Alignas,
_Alignof,
_Atomic,
_Bool,
_Complex,
_Generic,
_Imaginary,
_Noreturn,
_Static_assert,
_Thread_local,
auto,
@"break",
case,
char,
@"const",
@"continue",
default,
do,
double,
@"else",
@"enum",
@"extern",
float,
@"for",
goto,
@"if",
@"inline",
int,
long,
register,
restrict,
@"return",
short,
signed,
sizeof,
static,
@"struct",
@"switch",
typedef,
@"union",
unsigned,
void,
@"volatile",
@"while",
pub const map: std.StaticStringMap(Keyword) = blk: {
const fields = @typeInfo(Keyword).@"enum".fields;
var kvs_list: [fields.len]struct { []const u8, Keyword } = undefined;
for (fields, 0..) |field, i| {
kvs_list[i] = .{ field.name, @field(Keyword, field.name) };
}
break :blk .initComptime(kvs_list);
};
pub fn isKeyword(identifier: []const u8) ?Keyword {
return map.get(identifier);
}
};

View File

@@ -0,0 +1,70 @@
const std = @import("std");
pub const Punctuator = enum(u32) {
// three characters
@"..." = strToInt3("..."),
@"<<=" = strToInt3("<<="),
@">>=" = strToInt3(">>="),
// two characters
@"--" = strToInt2("--"),
@"-=" = strToInt2("-="),
@"->" = strToInt2("->"),
@"!=" = strToInt2("!="),
@"*=" = strToInt2("*="),
@"/=" = strToInt2("/="),
@"&&" = strToInt2("&&"),
@"&=" = strToInt2("&="),
@"##" = strToInt2("##"),
@"%=" = strToInt2("%="),
@"^=" = strToInt2("^="),
@"++" = strToInt2("++"),
@"+=" = strToInt2("+="),
@"<<" = strToInt2("<<"),
@"<=" = strToInt2("<="),
@"==" = strToInt2("=="),
@">=" = strToInt2(">="),
@">>" = strToInt2(">>"),
@"|=" = strToInt2("|="),
@"||" = strToInt2("||"),
// single character
@"-" = strToInt1("-"),
@"," = strToInt1(","),
@";" = strToInt1(";"),
@":" = strToInt1(":"),
@"!" = strToInt1("!"),
@"?" = strToInt1("?"),
@"." = strToInt1("."),
@"(" = strToInt1("("),
@")" = strToInt1(")"),
@"[" = strToInt1("["),
@"]" = strToInt1("]"),
@"{" = strToInt1("{"),
@"}" = strToInt1("}"),
@"*" = strToInt1("*"),
@"/" = strToInt1("/"),
@"&" = strToInt1("&"),
@"#" = strToInt1("#"),
@"%" = strToInt1("%"),
@"^" = strToInt1("^"),
@"+" = strToInt1("+"),
@"<" = strToInt1("<"),
@"=" = strToInt1("="),
@">" = strToInt1(">"),
@"|" = strToInt1("|"),
@"~" = strToInt1("~"),
pub const line_continuation_lf = strToInt2("\\\n");
pub const line_continuation_crlf = strToInt3("\\\r\n");
};
fn strToInt1(str: *const [1]u8) u32 {
return @as(u8, @bitCast(str.*));
}
fn strToInt2(str: *const [2]u8) u32 {
return @as(u16, @bitCast(str.*));
}
fn strToInt3(str: *const [3]u8) u32 {
return @as(u24, @bitCast(str.*));
}

View File

@@ -0,0 +1,16 @@
const std = @import("std");
const Builtin = @import("Builtin.zig").Builtin;
const Constant = @import("Constant.zig").Constant;
const Keyword = @import("Keyword.zig").Keyword;
const Punctuator = @import("Punctuator.zig").Punctuator;
pub const Token = union(enum) {
builtin: Builtin,
constant: Constant,
keyword: Keyword,
punctuator: Punctuator,
identifier: []const u8,
string_literal: [:0]const u8,
wide_string_literal: [:0]const u32,
};

View File

@@ -0,0 +1,263 @@
const std = @import("std");
const Self = @This();
const Builtin = @import("Builtin.zig").Builtin;
const Keyword = @import("Keyword.zig").Keyword;
const Punctuator = @import("Punctuator.zig").Punctuator;
const Token = @import("Token.zig").Token;
const Utf8Iterator = @import("Utf8Iterator.zig");
pub const max_string_length = 4096;
pub const max_wide_string_length = 4096;
filename: []const u8 = &.{},
it: Utf8Iterator = .init(&.{}),
defines: std.StringHashMapUnmanaged([]Token) = .{},
/// Bounded, preallocated with the capacity of `max_string_length`.
string: std.ArrayList(u8),
/// Bounded, preallocated with the capacity of `max_wide_string_length`.
wide_string: std.ArrayList(u32),
pub fn init(arena_allocator: std.mem.Allocator) !Self {
const string_buffer = try arena_allocator.alloc(u8, max_string_length);
const wide_string_buffer = try arena_allocator.alloc(u32, max_wide_string_length);
return .{
.string = .initBuffer(string_buffer),
.wide_string = .initBuffer(wide_string_buffer),
};
}
pub fn setSource(self: *Self, filename: []const u8, code: []const u8) void {
self.filename = filename;
self.it = .init(code);
}
pub fn nextToken(self: *Self, arena_allocator: std.mem.Allocator) !?Token {
try self.skipWhitespace();
// TODO Skip C and C++ style comments
// TODO Preprocessor directives
const cp = try self.peekCodepointSkipLineContinuation() orelse return null;
switch (cp) {
// Identifier start
'A'...'Z', '_', 'a'...'z', 128...std.math.maxInt(u21) => {
// This is an identifier, with the possible exception of:
// - wide string: L"
// - wide char: L'
// - any keyword
if (cp == 'L') {
const state = self.it.save();
self.it.advanceCodepoint(cp);
const cp2 = try self.peekCodepointSkipLineContinuation() orelse 0;
switch (cp2) {
// Wide string
'\"' => {
self.it.advanceCodepoint(cp2);
self.wide_string.clearRetainingCapacity();
// TODO Parse wide string
},
// Wide char
'\'' => {
self.it.advanceCodepoint(cp2);
// TODO Parse wide char
},
// Identifier or keyword
else => {
self.it.restore(state);
},
}
}
const identifier_start = self.it.ptr;
self.it.advanceCodepoint(cp);
var next_cp = try self.peekCodepointSkipLineContinuation();
while (next_cp != null and isIdentifierMiddle(next_cp.?)) {
self.it.advanceCodepoint(next_cp.?);
next_cp = try self.peekCodepointSkipLineContinuation();
}
const identifier = self.it.str[identifier_start..self.it.ptr];
// TODO Preprocessor
if (Keyword.isKeyword(identifier)) |keyword| {
return .{ .keyword = keyword };
} else if (Builtin.isBuiltin(identifier)) |builtin| {
return .{ .builtin = builtin };
} else {
return .{ .identifier = try arena_allocator.dupe(u8, identifier) };
}
},
// String
'\"' => {
self.it.advanceCodepoint(cp);
self.string.clearRetainingCapacity();
// TODO Parse string
},
// Char
'\'' => {
self.it.advanceCodepoint(cp);
// TODO Parse char
},
else => {},
}
// Higher code points should've been already handled. The code below may
// assume that `cp` is an ASCII character.
std.debug.assert(cp < 128);
// TODO Numeric constants
const cp3 = self.it.peekThreeBytes().?;
switch (cp3 & 0x00_FF_FF_FF) {
inline @intFromEnum(Punctuator.@"..."),
@intFromEnum(Punctuator.@"<<="),
@intFromEnum(Punctuator.@">>="),
=> |p| {
self.it.ptr += 3;
self.it.col += 3;
return .{
.punctuator = @enumFromInt(p),
};
},
else => {},
}
switch (cp3 & 0x00_00_FF_FF) {
inline @intFromEnum(Punctuator.@"--"),
@intFromEnum(Punctuator.@"-="),
@intFromEnum(Punctuator.@"->"),
@intFromEnum(Punctuator.@"!="),
@intFromEnum(Punctuator.@"*="),
@intFromEnum(Punctuator.@"/="),
@intFromEnum(Punctuator.@"&&"),
@intFromEnum(Punctuator.@"&="),
@intFromEnum(Punctuator.@"##"),
@intFromEnum(Punctuator.@"%="),
@intFromEnum(Punctuator.@"^="),
@intFromEnum(Punctuator.@"++"),
@intFromEnum(Punctuator.@"+="),
@intFromEnum(Punctuator.@"<<"),
@intFromEnum(Punctuator.@"<="),
@intFromEnum(Punctuator.@"=="),
@intFromEnum(Punctuator.@">="),
@intFromEnum(Punctuator.@">>"),
@intFromEnum(Punctuator.@"|="),
@intFromEnum(Punctuator.@"||"),
=> |p| {
self.it.ptr += 2;
self.it.col += 2;
return .{
.punctuator = @enumFromInt(p),
};
},
else => {},
}
switch (cp3 & 0x00_00_00_FF) {
inline @intFromEnum(Punctuator.@"-"),
@intFromEnum(Punctuator.@","),
@intFromEnum(Punctuator.@";"),
@intFromEnum(Punctuator.@":"),
@intFromEnum(Punctuator.@"!"),
@intFromEnum(Punctuator.@"?"),
@intFromEnum(Punctuator.@"."),
@intFromEnum(Punctuator.@"("),
@intFromEnum(Punctuator.@")"),
@intFromEnum(Punctuator.@"["),
@intFromEnum(Punctuator.@"]"),
@intFromEnum(Punctuator.@"{"),
@intFromEnum(Punctuator.@"}"),
@intFromEnum(Punctuator.@"*"),
@intFromEnum(Punctuator.@"/"),
@intFromEnum(Punctuator.@"&"),
@intFromEnum(Punctuator.@"#"),
@intFromEnum(Punctuator.@"%"),
@intFromEnum(Punctuator.@"^"),
@intFromEnum(Punctuator.@"+"),
@intFromEnum(Punctuator.@"<"),
@intFromEnum(Punctuator.@"="),
@intFromEnum(Punctuator.@">"),
@intFromEnum(Punctuator.@"|"),
@intFromEnum(Punctuator.@"~"),
=> |p| {
self.it.ptr += 1;
self.it.col += 1;
return .{
.punctuator = @enumFromInt(p),
};
},
else => {},
}
return error.InvalidToken;
}
fn peekCodepointSkipLineContinuation(self: *Self) !?u21 {
while (self.skipLineContinuation()) {}
const cp = try self.it.peekCodepoint();
return cp;
}
/// Line continuation is defined as a backslash followed imediatelly by LF or
/// CRLF. Return whether a line continuation was encountered and therefore
/// skipped past.
fn skipLineContinuation(self: *Self) bool {
if (self.it.peekThreeBytes()) |b| {
@branchHint(.likely);
if (b & 0x00_00_FF_FF == Punctuator.line_continuation_lf) {
@branchHint(.unlikely);
self.it.ptr += 2;
self.it.line += 1;
self.it.col = 1;
return true;
} else if (b & 0x00_FF_FF_FF == Punctuator.line_continuation_crlf) {
@branchHint(.unlikely);
self.it.ptr += 3;
self.it.line += 1;
self.it.col = 1;
return true;
}
}
return false;
}
fn skipWhitespace(self: *Self) !void {
while (try self.peekCodepointSkipLineContinuation()) |cp| {
switch (cp) {
// <Character Tabulation> (HT, TAB)
0x0009,
// <End of Line> (EOL, LF, NL)
0x000A,
// <Line Tabulation> (VT)
0x000B,
// <Form Feed> (FF)
0x000C,
// <Carriage Return> (CR)
0x000D,
// Space (SP)
0x0020,
=> self.it.advanceCodepoint(cp),
else => return,
}
}
}
fn isIdentifierMiddle(code_point: u21) bool {
// zig fmt: off
return code_point >= '0' and code_point <= '9'
or code_point >= 'A' and code_point <= 'Z'
or code_point == '_'
or code_point >= 'a' and code_point <= 'z'
or code_point >= 128;
// zig fmt: on
}

View File

@@ -0,0 +1,99 @@
const std = @import("std");
const Self = @This();
str: []const u8,
ptr: usize,
line: usize,
col: usize,
pub const State = struct {
ptr: usize,
line: usize,
col: usize,
};
pub fn init(str: []const u8) Self {
return .{
.str = str,
.ptr = 0,
.line = 1,
.col = 1,
};
}
pub fn save(self: Self) State {
return .{
.ptr = self.ptr,
.line = self.line,
.col = self.col,
};
}
pub fn restore(self: *Self, state: State) void {
self.ptr = state.ptr;
self.line = state.line;
self.col = state.col;
}
pub fn peekByte(self: *Self) ?u8 {
if (self.ptr >= self.str.len) {
return null;
}
return self.str[self.ptr];
}
pub fn peekCodepoint(self: Self) !?u21 {
if (self.ptr >= self.str.len) {
return null;
}
const cp_len = std.unicode.utf8ByteSequenceLength(self.str[self.ptr]) catch return error.InvalidUtf8;
if (self.ptr + cp_len > self.str.len) return error.InvalidUtf8;
const cp_slice = self.str[self.ptr .. self.ptr + cp_len];
const cp = std.unicode.utf8Decode(cp_slice) catch return error.InvalidUtf8;
return cp;
}
pub fn peekThreeBytes(self: Self) ?u32 {
var bytes: [3]u8 = .{ 0, 0, 0 };
const bytes_left = self.str.len - self.ptr;
sw: switch (bytes_left) {
0 => return null,
1 => {
bytes[0] = self.str[self.ptr];
return @as(u24, @bitCast(bytes));
},
2 => {
bytes[1] = self.str[self.ptr + 1];
continue :sw 1;
},
else => {
bytes[2] = self.str[self.ptr + 2];
continue :sw 2;
},
}
}
/// Call with value returned by `peekCodepoint`.
pub fn advanceCodepoint(self: *Self, cp: u21) void {
std.debug.assert(blk: {
const actual_cp = self.peekCodepoint() catch break :blk false;
break :blk cp == actual_cp;
});
const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable;
self.ptr += cp_len;
if (cp == '\n') {
self.line += 1;
// NOTE Columns start as 1, it will be incremented below.
self.col = 0;
}
self.col += 1;
}

201
packages/cjit/src/types.zig Normal file
View File

@@ -0,0 +1,201 @@
const std = @import("std");
pub const Type = union(enum) {
signed_char: void,
signed_short: void,
signed_int: void,
signed_long: void,
signed_long_long: void,
unsigned_char: void,
unsigned_short: void,
unsigned_int: void,
unsigned_long: void,
unsigned_long_long: void,
float: void,
double: void,
long_double: void,
void: void,
noreturn: void,
char: void,
bool: void,
@"enum": *const Enum,
@"struct": *const Struct,
@"union": *const Union,
array: *const Array,
function: *const Function,
pointer: *const Pointer,
pub fn sizeOf(self: Type) ?usize {
return switch (self) {
.signed_char => 1,
.signed_short => 2,
.signed_int => 4,
.signed_long => 8,
.signed_long_long => 8,
.unsigned_char => 1,
.unsigned_short => 2,
.unsigned_int => 4,
.unsigned_long => 8,
.unsigned_long_long => 8,
.float => 4,
.double => 8,
.long_double => 8,
.void => null,
.noreturn => null,
.char => 1,
.bool => 1,
.@"enum" => 4,
.@"struct" => |s| s.size,
.@"union" => |u| u.size,
.array => |a| if (a.length) |l| (if (sizeOf(a.child)) |c| l * c else null) else 8,
.function => null,
.pointer => 8,
};
}
pub fn alignOf(self: Type) ?u16 {
return switch (self) {
.signed_char => 1,
.signed_short => 2,
.signed_int => 4,
.signed_long => 8,
.signed_long_long => 8,
.unsigned_char => 1,
.unsigned_short => 2,
.unsigned_int => 4,
.unsigned_long => 8,
.unsigned_long_long => 8,
.float => 4,
.double => 8,
.long_double => 8,
.void => null,
.noreturn => null,
.char => 1,
.bool => 1,
.@"enum" => 4,
.@"struct" => |s| s.@"align",
.@"union" => |u| u.@"align",
.array => |a| if (a.length != null) alignOf(a.child) else 8,
.function => null,
.pointer => 8,
};
}
};
pub const Enum = struct {
name: []const u8,
constants: []const EnumConstant,
};
pub const EnumConstant = struct {
name: []const u8,
value: i32,
inferred: bool,
};
pub const Struct = struct {
name: []const u8,
fields: []const StructField,
size: usize,
@"align": u16,
};
pub const StructField = struct {
name: []const u8,
type: Type,
offset: usize,
};
pub const Union = struct {
name: []const u8,
fields: []const UnionField,
size: usize,
@"align": u16,
};
pub const UnionField = struct {
name: []const u8,
type: Type,
};
pub const Array = struct {
child: Type,
length: ?usize,
};
pub const Function = struct {
arguments: []const FunctionArgument,
@"return": Type,
};
pub const FunctionArgument = struct {
name: []const u8,
type: Type,
};
pub const Pointer = struct {
child: Type,
@"const": bool,
@"volatile": bool,
};
pub fn isCompatible(comptime ZigType: type, c_type: Type) bool {
return switch (@typeInfo(ZigType)) {
.type => false,
.void => c_type == .void,
.bool => c_type == .bool,
.noreturn => c_type == .noreturn,
.int => |zig_int| switch (c_type) {
.signed_char => zig_int.signedness == .signed and zig_int.bits == 8,
.signed_short => zig_int.signedness == .signed and zig_int.bits == 16,
.signed_int => zig_int.signedness == .signed and zig_int.bits == 32,
.signed_long => zig_int.signedness == .signed and zig_int.bits == 64,
.signed_long_long => zig_int.signedness == .signed and zig_int.bits == 64,
.unsigned_char => zig_int.signedness == .unsigned and zig_int.bits == 8,
.unsigned_short => zig_int.signedness == .unsigned and zig_int.bits == 16,
.unsigned_int => zig_int.signedness == .unsigned and zig_int.bits == 32,
.unsigned_long => zig_int.signedness == .unsigned and zig_int.bits == 64,
.unsigned_long_long => zig_int.signedness == .unsigned and zig_int.bits == 64,
.char => zig_int.bits = 8,
else => false,
},
.float => |zig_float| switch (c_type) {
.float => zig_float.bits = 32,
.double => zig_float.bits = 64,
.long_double => zig_float.bits = 64,
},
.pointer => @compileError("TODO"),
.array => @compileError("TODO"),
.@"struct" => @compileError("TODO"),
.comptime_float => false,
.comptime_int => false,
.undefined => true,
.null => @compileError("TODO"),
.optional => @compileError("TODO"),
.error_union => false,
.error_set => false,
.@"enum" => @compileError("TODO"),
.@"union" => @compileError("TODO"),
.@"fn" => false,
.@"opaque" => @compileError("TODO"),
.frame => false,
.@"anyframe" => false,
.vector => false,
.enum_literal => false,
};
}

View File

@@ -0,0 +1,445 @@
const std = @import("std");
const tokens = @import("tokens.zig");
const types = @import("types.zig");
const Location = Runtime.Location;
const Runtime = @import("Runtime.zig");
const StackValue = @import("StackValue.zig");
const Type = types.Type;
pub const Register = union(enum) {
gpr: Gpr,
xmm: Xmm,
};
pub const Gpr = enum(u4) {
rax = 0,
rcx = 1,
rdx = 2,
rbx = 3,
rsp = 4,
rbp = 5,
rsi = 6,
rdi = 7,
r8 = 8,
r9 = 9,
r10 = 10,
r11 = 11,
r12 = 12,
r13 = 13,
r14 = 14,
r15 = 15,
pub fn reg(self: Gpr) u3 {
return @truncate(@intFromEnum(self));
}
pub fn x(self: Gpr) bool {
return @intFromEnum(self) & 0b1000 != 0;
}
};
pub const Xmm = enum(u4) {
xmm0 = 0,
xmm1 = 1,
xmm2 = 2,
xmm3 = 3,
xmm4 = 4,
xmm5 = 5,
xmm6 = 6,
xmm7 = 7,
xmm8 = 8,
xmm9 = 9,
xmm10 = 10,
xmm11 = 11,
xmm12 = 12,
xmm13 = 13,
xmm14 = 14,
xmm15 = 15,
pub fn reg(self: Xmm) u3 {
return @truncate(@intFromEnum(self));
}
pub fn x(self: Xmm) bool {
return @intFromEnum(self) & 0b1000 != 0;
}
};
// --- EMIT HELPERS ------------------------------------------------------------
pub const Rex = packed struct(u8) {
b: bool = false,
x: bool = false,
r: bool = false,
w: bool = false,
/// MUST always be the default value
prefix: u4 = 0b0100,
};
pub const ModRM = packed struct(u8) {
rm: u3,
reg: u3,
mod: u2,
};
pub const SIB = packed struct(u8) {
base: u3,
index: u3,
scale: u2,
};
pub fn op16(rt: *Runtime) !void {
try rt.sections.text.writeByte(0x66, rt.allocator);
}
pub fn rex(rt: *Runtime, value: Rex) !void {
try rt.sections.text.writeByte(@bitCast(value), rt.allocator);
}
pub fn op(rt: *Runtime, value: u8) !void {
try rt.sections.text.writeByte(value, rt.allocator);
}
pub fn op2(rt: *Runtime, v0: u8, v1: u8) !void {
const bytes: [2]u8 = .{ v0, v1 };
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
pub fn op3(rt: *Runtime, v0: u8, v1: u8, v2: u8) !void {
const bytes: [3]u8 = .{ v0, v1, v2 };
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
pub fn modrm(rt: *Runtime, value: ModRM) !void {
try rt.sections.text.writeByte(@bitCast(value), rt.allocator);
}
pub fn sib(rt: *Runtime, value: SIB) !void {
try rt.sections.text.writeByte(@bitCast(value), rt.allocator);
}
pub fn imm8(rt: *Runtime, value: u8) !void {
try rt.sections.text.writeByte(value, rt.allocator);
}
pub fn imm16(rt: *Runtime, value: u16) !void {
var bytes: [2]u8 = undefined;
std.mem.writeInt(u16, &bytes, value, .little);
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
pub fn imm32(rt: *Runtime, value: u32) !void {
var bytes: [4]u8 = undefined;
std.mem.writeInt(u32, &bytes, value, .little);
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
pub fn imm64(rt: *Runtime, value: u64) !void {
var bytes: [8]u8 = undefined;
std.mem.writeInt(u64, &bytes, value, .little);
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
pub fn disp8(rt: *Runtime, value: i8) !void {
try rt.sections.text.writeByte(@bitCast(value), rt.allocator);
}
pub fn disp32(rt: *Runtime, value: i32) !void {
var bytes: [4]u8 = undefined;
std.mem.writeInt(i32, &bytes, value, .little);
try rt.sections.text.writeBytes(&bytes, rt.allocator);
}
// -----------------------------------------------------------------------------
pub const Operation = enum {
add,
bit_and,
bit_not,
bit_or,
bit_xor,
bool_and,
bool_not,
bool_or,
cmp_eq,
cmp_gt,
cmp_gte,
cmp_lt,
cmp_lte,
cmp_neq,
div,
mod,
mul,
neg,
sar,
shl,
shr,
sub,
};
pub fn opFloat(rt: *Runtime, operation: Operation) !void {
_ = rt;
_ = operation;
}
pub fn opInt(rt: *Runtime, operation: Operation) !void {
_ = rt;
_ = operation;
}
pub fn cvtIntToFloat(rt: *Runtime) !void {
_ = rt;
}
pub fn cvtFloatToInt(rt: *Runtime) !void {
_ = rt;
}
pub fn cvtFloatToFloat(rt: *Runtime, target: Type) !void {
const top = vsTop(rt);
switch (top.c_type) {
.float => switch (target) {
.float => {
// do nothing
},
.double, .long_double => {
// CVTSS2SD xmm1, xmm2/m32
// F3 0F 5A /r
},
else => unreachable,
},
.double, .long_double => switch (target) {
.float => {
// CVTSD2SS xmm1, xmm2/m64
// F2 0F 5A /r
},
.double, .long_double => {
// do nothing
},
else => unreachable,
},
else => unreachable,
}
}
// --- LOAD AND STORE ----------------------------------------------------------
/// Load value into register. The value must be 1, 2, 4 or 8 bytes long.
pub fn load(rt: *Runtime, dst_register: Register, src_value: *const StackValue) !void {
const size = src_value.c_type.sizeOf().?;
std.debug.assert(size == 1 or size == 2 or size == 4 or size == 8);
switch (src_value.value) {
.register => |src_register| {
if (std.meta.eql(dst_register, src_register)) return;
// TODO
},
.constant => |constant| switch (dst_register) {
.gpr => |dest_gpr| switch (size) {
1 => {
// MOV r8, imm8
// B0+ rb ib
if (@intFromEnum(dest_gpr) >= 4) {
// NOTE spl, bpl, sil and dil need an empty REX prefix,
// otherwise ah, ch, dh and bh would be used.
try rex(rt, .{ .r = dest_gpr.x() });
}
try op(rt, 0xB0 | @as(u8, dest_gpr.reg()));
try imm8(rt, @truncate(constant));
},
2 => {
// MOV r16, imm16
// B8+ rw iw
try op16(rt);
if (@intFromEnum(dest_gpr) >= 8) {
try rex(rt, .{ .r = dest_gpr.x() });
}
try op(rt, 0xB8 | @as(u8, dest_gpr.reg()));
try imm16(rt, @truncate(constant));
},
4 => {
// MOV r32, imm32
// B8+ rd id
if (@intFromEnum(dest_gpr) >= 8) {
try rex(rt, .{ .r = dest_gpr.x() });
}
try op(rt, 0xB8 | @as(u8, dest_gpr.reg()));
try imm32(rt, @truncate(constant));
},
8 => {
// MOV r64, imm64
// REX.W + B8+ rd io
try rex(rt, .{ .r = dest_gpr.x(), .w = true });
try op(rt, 0xB8 | @as(u8, dest_gpr.reg()));
try imm64(rt, constant);
},
else => unreachable,
},
.xmm => |dest_xmm| {
var bytes: [8]u8 = undefined;
std.mem.writeInt(u64, &bytes, @truncate(constant), .little);
const data = bytes[0..size];
// MOVD xmm, r/m32
// 66 0F 6E /r
// MOVQ xmm, r/m64
// 66 REX.W 0F 6E /r
try op(rt, 0x66);
if (@intFromEnum(dest_xmm) >= 8 or size == 8) {
try rex(rt, .{ .r = dest_xmm.x(), .w = size == 8 });
}
try op2(rt, 0x0F, 0x6E);
// [rip + disp32]
try modrm(rt, .{ .mod = 0b00, .reg = dest_xmm.reg(), .rm = Gpr.rbp.reg() });
try allocRodataDisp32(rt, data);
},
},
.symbol => |symbol| {},
.stack => |disp| {
const disp_small = std.math.minInt(i8) <= disp and disp <= std.math.maxInt(i8);
const rex_prefix: Rex = .{
.r = switch (dst_register) {
.gpr => |dest_gpr| dest_gpr.x(),
.xmm => |dest_xmm| dest_xmm.x(),
},
.w = size == 8,
};
const mod: u2 = if (disp_small) 0b01 else 0b10;
const reg: u3 = switch (dst_register) {
.gpr => |dest_gpr| dest_gpr.reg(),
.xmm => |dest_xmm| dest_xmm.reg(),
};
switch (dst_register) {
.gpr => |dest_gpr| {
switch (size) {
1 => {
// MOV r8, r/m8
// 8A /r
if (@intFromEnum(dest_gpr) >= 4) {
// NOTE spl, bpl, sil and dil need an empty REX prefix,
// otherwise ah, ch, dh and bh would be used.
try rex(rt, rex_prefix);
}
try op(rt, 0x8A);
},
2 => {
// MOV r16, r/m16
// 8B /r
try op16(rt);
if (@intFromEnum(dest_gpr) >= 8) {
try rex(rt, rex_prefix);
}
try op(rt, 0x8B);
},
4 => {
// MOV r32, r/m32
// 8B /r
if (@intFromEnum(dest_gpr) >= 8) {
try rex(rt, rex_prefix);
}
try op(rt, 0x8B);
},
8 => {
// MOV r64, r/m64
// REX.W + 8B /r
try rex(rt, rex_prefix);
try op(rt, 0x8B);
},
else => unreachable,
}
},
.xmm => |dest_xmm| {
// MOVD xmm, r/m32
// 66 0F 6E /r
// MOVQ xmm, r/m64
// 66 REX.W 0F 6E /r
try op(rt, 0x66);
if (@intFromEnum(dest_xmm) >= 8 or size == 8) {
try rex(rt, rex_prefix);
}
try op2(rt, 0x0F, 0x6E);
},
}
// [rbp + disp8/32]
try modrm(rt, .{ .mod = mod, .reg = reg, .rm = Gpr.rbp.reg() });
if (disp_small) {
try disp8(rt, @intCast(disp));
} else {
try disp32(rt, disp);
}
},
.cpu_flags => {},
}
}
/// Store register into value.
pub fn store(rt: *Runtime, dst_value: *const StackValue, src_register: Register) !void {
const size = dst_value.c_type.sizeOf().?;
std.debug.assert(size == 1 or size == 2 or size == 4 or size == 8);
}
// --- STACK OPERATIONS --------------------------------------------------------
/// Caller asserts that the stack is not empty.
pub fn vsTop(rt: *Runtime) *StackValue {
const vs = rt.virtual_stack.items;
return &vs[vs.len - 1];
}
/// Caller asserts that the stack has at least two values.
pub fn vsSwap(rt: *Runtime) void {
const vs = rt.virtual_stack.items;
std.mem.swap(StackValue, &vs[vs.len - 1], &vs[vs.len - 2]);
}
/// Ensure the top of the stack is in an XMM register. Returns the id of the
/// register. Caller asserts that the stack is not empty.
pub fn vsEnsureXmm(rt: *Runtime) Xmm {
const top = vsTop(rt);
switch (top.value) {
.register => {},
.constant => {},
.symbol => {},
.stack => {},
.cpu_flags => {},
}
}
/// Ensure the top of the stack is in a GPR register. Returns the id of the
/// register. Caller asserts that teh stack is not empty.
pub fn vsEnsureGpr(rt: *Runtime) Gpr {
const top = vsTop(rt);
switch (top.value) {
.register => {},
.constant => {},
.symbol => {},
.stack => {},
.cpu_flags => {},
}
}
// --- DATA ALLOCATIONS --------------------------------------------------------
/// Reserve `data.len` bytes in rodata and fill it with `data`, then add a
/// placeholder dips32 part of an instruction and a relocation entry for it.
pub fn allocRodataDisp32(rt: *Runtime, data: []const u8) !void {
const addr = rt.sections.text.data.items.len;
const location: Location = .{ .rodata = rt.sections.rodata.data.items.len };
try rt.sections.rodata.writeBytes(data, rt.allocator);
try disp32(rt, 0);
try rt.relocation_table.append(rt.allocator, .{
.addr = addr,
.location = location,
.type = .rip_disp32,
});
}

View File

@@ -0,0 +1,32 @@
const std = @import("std");
const cjit = @import("cjit");
fn add(a: i32, b: i32) callconv(cjit.call) i32 {
return a + b;
}
test {
var rt: cjit.Runtime = try .init(std.testing.allocator);
defer rt.deinit();
try rt.compile("test.c",
\\int add(int a, int b);
\\
\\int add_one(int x)
\\{
\\ return add(x, 1);
\\}
);
try rt.setSymbol(fn (i32, i32) callconv(cjit.call) i32, "add", &add);
try rt.link();
const add_one = rt.getSymbol(fn (i32) callconv(cjit.call) i32, "add_one").?;
try std.testing.expectEqual(-9, add_one(-10));
try std.testing.expectEqual(11, add_one(10));
const add_ptr = rt.getSymbol(fn (i32, i32) callconv(cjit.call) i32, "add").?;
try std.testing.expectEqual(add_ptr, &add);
}

View File

@@ -195,7 +195,7 @@ const import = struct {
no_add: bool = false,
/// Internal use.
no_exotic: bool = false,
_pad14: u18 = 0,
_pad18: u14 = 0,
};
pub const JS_EVAL = packed struct(u32) {
@@ -599,7 +599,7 @@ const import = struct {
pub const Allocator = struct {
const alignment_bytes = 16;
const alignment: std.mem.Alignment = .fromByteUnits(alignment_bytes);
const alignment = std.mem.Alignment.fromByteUnits(alignment_bytes);
const AllocationPtr = *align(alignment_bytes) anyopaque;
const AllocationSlice = []align(alignment_bytes) u8;
@@ -1168,7 +1168,7 @@ pub const Value = extern struct {
}
pub fn getClassId(self: Value) ClassId {
return .{ .class_id = import.JS_GetClassID(self) };
return .{ .class_id = import.JS_GetClassID(self.value) };
}
};
@@ -1182,7 +1182,7 @@ pub const ClassId = extern struct {
pub const invalid: ClassId = .{ .class_id = 0 };
pub fn new() ClassId {
var class_id: import.JSClassId = 0;
var class_id: import.JSClassID = 0;
return .{ .class_id = import.JS_NewClassID(&class_id) };
}
};

View File

@@ -1,11 +1,38 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const vm_dep = b.dependency("vecmath", .{});
const vm_module = vm_dep.module("vecmath");
const target = b.standardTargetOptions(.{});
const root_module = b.addModule("media", .{
.root_source_file = b.path("src/root.zig"),
const vm_dep = b.dependency("vecmath", .{
.target = target,
});
root_module.addImport("vecmath", vm_module);
const vm_mod = vm_dep.module("vecmath");
const mod = b.addModule("media", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.link_libc = true,
.imports = &.{
.{ .name = "vecmath", .module = vm_mod },
},
});
mod.addCSourceFile(.{
.file = b.path("src/stbi/stb_image.c"),
.flags = &.{
"-std=c17",
"-Wall",
"-Wextra",
},
.language = .c,
});
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}

View File

@@ -1,7 +1,7 @@
.{
.name = .media,
.version = "0.0.0",
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.paths = .{
"src",
"build.zig",

BIN
packages/media/jpeg-specification.pdf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
packages/media/png-specification.pdf (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,25 @@
const std = @import("std");
const Self = @This();
/// The number of bytes necessary to confirm that a buffer contains this media
/// format.
magic_length: usize,
/// The number of bytes necessary to confirm that a buffer contains this media
/// format and to extract metadata stored at the beggining, if any. Must be at
/// least as big as `magic_length`.
info_length: usize,
/// The file extension usually associated with this media format; all in
/// lowercase and without the dot character.
extension: []const u8,
/// The media type (aka MIME type or Content-Type) usually associated with this
/// media format; not necessarily officially registered.
media_type: []const u8,
/// Confirm whether a buffer contains this media format. The buffer doesn't have
/// to contain the entire file, only its beginning. The caller asserts that
/// `buffer` is at least `magic_length` bytes long.
isFormat: *const fn (buffer: []const u8) bool,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -9,14 +9,26 @@ pub const Static = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / sample_rate);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const Sample = extern struct {
left: i16,
right: i16,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const Stream = struct {
source: std.io.Reader,
source: *std.Io.Reader,
sample_rate: u32,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -19,11 +19,15 @@ pub fn Static(comptime W: u32, comptime H: u32) type {
}
pub fn fill(self: *@This(), color: vm.Color) void {
@memset(self.data, color);
@memset(&self.data, color);
}
};
}
test "Static - refAllDecls" {
std.testing.refAllDecls(Static(16, 16));
}
pub const Dynamic = struct {
width: u32,
height: u32,
@@ -65,4 +69,55 @@ pub const Dynamic = struct {
pub fn fill(self: *@This(), color: vm.Color) void {
@memset(self.data[0 .. self.width * self.height], color);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const Hdr = struct {
width: u32,
height: u32,
data: [*]vm.ColorHdr,
pub fn initBuffer(width: u32, height: u32, buffer: []vm.ColorHdr) @This() {
std.debug.assert(buffer.len == width * height);
return .{
.width = width,
.height = height,
.data = buffer.ptr,
};
}
pub fn initAlloc(width: u32, height: u32, allocator: std.mem.Allocator) !@This() {
const buffer = try allocator.alloc(vm.ColorHdr, width * height);
return .{
.width = width,
.height = height,
.data = buffer.ptr,
};
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.data[0 .. self.width * self.height]);
}
pub fn getPixel(self: *const @This(), x: u32, y: u32) vm.ColorHdr {
std.debug.assert(x < self.width and y < self.height);
return self.data[y * self.width + x];
}
pub fn setPixel(self: *@This(), x: u32, y: u32, color: vm.ColorHdr) void {
std.debug.assert(x < self.width and y < self.height);
self.data[y * self.width + x] = color;
}
pub fn fill(self: *@This(), color: vm.ColorHdr) void {
@memset(self.data[0 .. self.width * self.height], color);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

151
packages/media/src/jpeg.zig Normal file
View File

@@ -0,0 +1,151 @@
const std = @import("std");
const Format = @import("Format.zig");
const format: Format = .{
.magic_length = magic.len,
// NOTE The information (like width and height) is not in a fixed position
.info_length = magic.len,
.extension = "jpeg",
.media_type = "image/jpeg",
.isFormat = isJpeg,
};
const magic = "\xFF\xD8\xFF";
const Info = union(enum) {
partial: void,
full: Header,
pub fn makeFull(full: Header) Info {
return .{ .full = full };
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
const Header = struct {
width: u32,
height: u32,
};
const Marker = enum(u8) {
/// Start of frame, Huffman coding, Baseline DCT
SOF_0 = 0xC0,
/// Start of frame, Huffman coding, Extended sequential DCT
SOF_1 = 0xC1,
/// Start of frame, Huffman coding, Progressive DCT
SOF_2 = 0xC2,
/// Start of frame, Huffman coding, Lossless (sequential)
SOF_3 = 0xC3,
/// Start of frame, Huffman coding, Differential sequential DCT
SOF_5 = 0xC5,
/// Start of frame, Huffman coding, Differential progressive DCT
SOF_6 = 0xC6,
/// Start of frame, Huffman coding, Differential lossless (sequential)
SOF_7 = 0xC7,
/// Start of frame, arithmetic coding, Extended sequential DCT
SOF_9 = 0xC9,
/// Start of frame, arithmetic coding, Progressive DCT
SOF_10 = 0xCA,
/// Start of frame, arithmetic coding, Lossless (sequential)
SOF_11 = 0xCB,
/// Start of frame, arithmetic coding, Differential sequential DCT
SOF_13 = 0xCD,
/// Start of frame, arithmetic coding, Differential progressive DCT
SOF_14 = 0xCE,
/// Start of frame, arithmetic coding, Differential lossless (sequential)
SOF_15 = 0xCF,
/// Define Huffman table(s)
DHT = 0xC4,
/// Define arithmetic coding conditioning(s)
DAC = 0xCC,
/// Start of image
SOI = 0xD8,
/// End of image
EOI = 0xD9,
/// Start of scan
SOS = 0xDA,
/// Define quantization table(s)
DQT = 0xDB,
/// Define number of lines
DNL = 0xDC,
/// Define restart interval
DRI = 0xDD,
/// Define hierarchical progression
DHP = 0xDE,
/// Expand reference component(s)
EXP = 0xDF,
/// Comment
COM = 0xFE,
/// For temporary private use in arithmetic coding
TEM = 0x01,
_,
/// Restart with modulo 8 count `m`
pub fn RST(m: u3) Marker {
return @enumFromInt(0xD0 | m);
}
pub fn isRST(self: Marker) ?u3 {
return if (@intFromEnum(self) & 0b1111_1000 == 0xD0) @intCast(@intFromEnum(self) & 0b0000_0111) else null;
}
/// Reserved for application segments
pub fn APP(n: u4) Marker {
return @enumFromInt(0xE0 | n);
}
pub fn isAPP(self: Marker) ?u3 {
return if (@intFromEnum(self) & 0b1111_0000 == 0xE0) @intCast(@intFromEnum(self) & 0b0000_1111) else null;
}
/// A standalone marker has no content and is not followed by segment length
/// parameter.
pub fn isStandalone(self: Marker) bool {
return self.isRST() != null or
self == .SOI or
self == .EOI or
self == .TEM;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
/// The caller asserts that the buffer is at least `format.magic_length` bytes
/// long.
pub fn isJpeg(buffer: []const u8) bool {
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
}
/// The caller asserts that the buffer is at least `format.info_length` bytes
/// long. The information is not in a fixed position, so you need to provide a
/// substantial amount of data, in the order of kilobytes. Depending on the
/// amount of metadata, the information might be in the first kilobyte or dozens
/// of kilobytes in.
///
/// This function returns:
///
/// - `null` when the buffer is not a JPEG or it's a malformed JPEG
/// - `.partial` when the buffer appears to be a part of a JPEG, but the
/// information could not be found within the buffer provided
/// - `.full` when the buffer appears to be a part of a JPEG and the information
/// was fully contained within the buffer provided
pub fn info(buffer: []const u8) ?Info {
std.debug.assert(buffer.len >= format.info_length);
if (!isJpeg(buffer)) {
return null;
}
@panic("TODO");
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -0,0 +1,41 @@
const std = @import("std");
const Format = @import("Format.zig");
const format: Format = .{
.magic_length = @max(magic_naked.len, magic_container.len),
.info_length = @max(magic_naked.len, magic_container.len),
.extension = "jxl",
.media_type = "image/jxl",
.isFormat = isJxl,
};
const magic_naked = "\xFF\x0A";
const magic_container = "\x00\x00\x00\x0CJXL \r\n\x87\n";
const Header = struct {};
/// The caller asserts that the buffer is at least `format.magic_length` bytes
/// long.
pub fn isJxl(buffer: []const u8) bool {
std.debug.assert(buffer.len >= format.magic_length);
return std.mem.startsWith(u8, buffer, magic_naked) or
std.mem.startsWith(u8, buffer, magic_container);
}
/// The caller asserts that the buffer is at least `format.info_length` bytes
/// long.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= format.info_length);
if (!isJxl(buffer)) {
return null;
}
@panic("TODO");
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

645
packages/media/src/png.zig Normal file
View File

@@ -0,0 +1,645 @@
const std = @import("std");
const Format = @import("Format.zig");
pub const format: Format = .{
.magic_length = magic.len,
// 4B - chunk length
// 4B - chunk type
// 13B - IHDR data
// 4B - checksum
.info_length = magic.len + 25,
.extension = "png",
.media_type = "image/png",
.isFormat = isPng,
};
const magic = "\x89PNG\r\n\x1A\n";
// --- IHDR --------------------------------------------------------------------
pub const Header = struct {
width: u32,
height: u32,
bit_depth: BitDepth,
color_type: ColorType,
compression_method: CompressionMethod,
filter_method: FilterMethod,
interlace_method: InterlaceMethod,
pub fn decode(chunk: Chunk) !Header {
std.debug.assert(chunk.chunk_type == .IHDR);
if (chunk.data.len != 13) return error.InvalidPng;
const width = std.mem.readInt(u32, chunk.data[0..4], .big);
const height = std.mem.readInt(u32, chunk.data[4..8], .big);
const bit_depth = chunk.data[8];
const color_type = chunk.data[9];
const compression_method = chunk.data[10];
const filter_method = chunk.data[11];
const interlace_method = chunk.data[12];
if (width == 0 or width > 0x7FFF_FFFF or
height == 0 or height > 0x7FFF_FFFF or
bit_depth == 0 or bit_depth > 16 or !std.math.isPowerOfTwo(bit_depth) or
color_type == 1 or color_type == 5 or color_type > 6 or
compression_method != 0 or
filter_method != 0 or
interlace_method > 1)
{
return error.InvalidPng;
}
switch (color_type) {
0 => {
// all bit depths allowed
},
2 => {
if (bit_depth < 8) return error.InvalidPng;
},
3 => {
if (bit_depth > 8) return error.InvalidPng;
},
4 => {
if (bit_depth < 8) return error.InvalidPng;
},
6 => {
if (bit_depth < 8) return error.InvalidPng;
},
else => unreachable,
}
return .{
.width = width,
.height = height,
.bit_depth = @enumFromInt(bit_depth),
.color_type = @enumFromInt(color_type),
.compression_method = @enumFromInt(compression_method),
.filter_method = @enumFromInt(filter_method),
.interlace_method = @enumFromInt(interlace_method),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const BitDepth = enum(u8) {
@"1" = 1,
@"2" = 2,
@"4" = 4,
@"8" = 8,
@"16" = 16,
pub fn range(self: BitDepth) usize {
return @as(usize, 2) << @intCast(@intFromEnum(self));
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const ColorType = enum(u8) {
grayscale = 0,
rgb = 2,
palette = 3,
grayscale_alpha = 4,
rgba = 6,
pub fn paletteUsed(self: ColorType) bool {
return @intFromEnum(self) & 0b0000_0001 != 0;
}
pub fn colorUsed(self: ColorType) bool {
return @intFromEnum(self) & 0b0000_0010 != 0;
}
pub fn alphaChannelUsed(self: ColorType) bool {
return @intFromEnum(self) & 0b0000_0100 != 0;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const CompressionMethod = enum(u8) {
flate = 0,
};
pub const FilterMethod = enum(u8) {
adaptive = 0,
};
pub const InterlaceMethod = enum(u8) {
none = 0,
adam7 = 1,
};
// --- PLTE --------------------------------------------------------------------
pub const Palette = struct {
len: u16,
entries: [256][3]u8,
pub fn asSlice(self: *const Palette) []const [3]u8 {
return self.entries[0..self.len];
}
pub fn decode(chunk: Chunk, ctx: Header) !Palette {
std.debug.assert(chunk.chunk_type == .PLTE);
const len = std.math.divExact(usize, chunk.data.len, 3) catch return error.InvalidPng;
switch (ctx.color_type) {
.grayscale => return error.InvalidPng,
.rgb => if (len < 1 or len > 256) return error.InvalidPng,
.palette => if (len < 1 or len > ctx.bit_depth.range()) return error.InvalidPng,
.grayscale_alpha => return error.InvalidPng,
.rgba => if (len < 1 or len > 256) return error.InvalidPng,
}
var entries: [256][3]u8 = undefined;
@memcpy(entries[0..len], @as([]const [3]u8, @ptrCast(chunk.data)));
return .{
.len = @intCast(len),
.entries = entries,
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// --- tRNS --------------------------------------------------------------------
pub const Transparency = union(enum) {
grayscale: u16,
rgb: [3]u16,
palette: TransparencyPalette,
pub fn initGrayscale(grayscale: u16) Transparency {
return .{ .grayscale = grayscale };
}
pub fn initRgb(rgb: [3]u16) Transparency {
return .{ .rgb = rgb };
}
pub fn initPalette(palette: TransparencyPalette) Transparency {
return .{ .palette = palette };
}
pub fn decode(chunk: Chunk, header: Header, maybe_palette: ?Palette) !Transparency {
std.debug.assert(chunk.chunk_type == .tRNS);
return switch (header.color_type) {
.grayscale => {
if (chunk.data.len != 2) return error.InvalidPng;
const grayscale = std.mem.readInt(u16, chunk.data[0..2], .big);
return .initGrayscale(grayscale);
},
.rgb => {
if (chunk.data.len != 6) return error.InvalidPng;
const r = std.mem.readInt(u16, chunk.data[0..2], .big);
const g = std.mem.readInt(u16, chunk.data[2..4], .big);
const b = std.mem.readInt(u16, chunk.data[4..6], .big);
return .initRgb(.{ r, g, b });
},
.palette => if (maybe_palette) |palette| {
const len = chunk.data.len;
if (len < 1 or len > palette.len) return error.InvalidPng;
var entries: [256]u8 = undefined;
@memcpy(entries[0..len], chunk.data[0..len]);
return .initPalette(.{
.len = @intCast(len),
.entries = entries,
});
} else {
return error.InvalidPng;
},
.grayscale_alpha => error.InvalidPng,
.rgba => error.InvalidPng,
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
const TransparencyPalette = struct {
len: u16,
entries: [256]u8,
pub fn asSlice(self: *const TransparencyPalette) []const u8 {
return self.entries[0..self.len];
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// --- gAMA --------------------------------------------------------------------
const Gamma = struct {
/// Gamma times `scale`, i.e. 100000, For example, a gamma of 1/2.2 would be
/// stored as 45455.
gamma: u32,
const scale = 100000;
pub fn initExponent(exponent: f32) Gamma {
const gamma: u32 = @intFromFloat(@round(exponent * scale));
return .{ .gamma = gamma };
}
pub fn asExponent(self: Gamma) f32 {
return @as(f32, @floatFromInt(self.gamma)) / scale;
}
pub fn decode(chunk: Chunk) Gamma {
std.debug.assert(chunk.chunk_type == .gAMA);
if (chunk.data.len != 4) return error.InvalidPng;
const gamma = std.mem.readInt(u32, chunk.data[0..4], .big);
return .{ .gamma = gamma };
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// --- cHRM --------------------------------------------------------------------
const Chromaticities = struct {
white_point_x: u32,
white_point_y: u32,
red_x: u32,
red_y: u32,
green_x: u32,
green_y: u32,
blue_x: u32,
blue_y: u32,
pub fn decode(chunk: Chunk) !Chunk {
std.debug.assert(chunk.chunk_type == .cHRM);
if (chunk.data.len != 32) return error.InvalidPng;
return .{
.white_point_x = std.mem.readInt(u32, chunk.data[0..4], .big),
.white_point_y = std.mem.readInt(u32, chunk.data[4..8], .big),
.red_x = std.mem.readInt(u32, chunk.data[8..12], .big),
.red_y = std.mem.readInt(u32, chunk.data[12..16], .big),
.green_x = std.mem.readInt(u32, chunk.data[16..20], .big),
.green_y = std.mem.readInt(u32, chunk.data[20..24], .big),
.blue_x = std.mem.readInt(u32, chunk.data[24..28], .big),
.blue_y = std.mem.readInt(u32, chunk.data[28..32], .big),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// --- sRGB --------------------------------------------------------------------
const RenderingIntent = enum(u8) {
/// Perceptual intent is for images preferring good adaptation to the output
/// device gamut at the expense of colorimetric accuracy, like photographs.
perceptual = 0,
/// Relative colorimetric intent is for images requiring color appearance
/// matching (relative to the output device white point), like logos.
relative_colorimetric = 1,
/// Saturation intent is for images preferring preservation of saturation at
/// the expense of hue and lightness, like charts and graphs.
saturation = 2,
/// Absolute colorimetric intent is for images requiring preservation of
/// absolute colorimetry, like proofs (previews of images destined for a
/// different output device).
absolute_colorimetric = 3,
pub fn decode(chunk: Chunk) RenderingIntent {
std.debug.assert(chunk.chunk_type == .sRGB);
if (chunk.data.len != 1) return error.InvalidPng;
const rendering_intent = chunk.data[0];
if (rendering_intent > 3) return error.InvalidPng;
return @enumFromInt(rendering_intent);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// --- pHYs --------------------------------------------------------------------
pub const PhysicalPixelDimensions = struct {
pixels_per_unit_x: u32,
pixels_per_unit_y: u32,
unit: PhysicalUnit,
pub fn decode(chunk: Chunk) !PhysicalPixelDimensions {
std.debug.assert(chunk.chunk_type == .pHYs);
if (chunk.data.len != 9) return error.InvalidPng;
const pixels_per_unit_x = std.mem.readInt(u32, chunk.data[0..4], .big);
const pixels_per_unit_y = std.mem.readInt(u32, chunk.data[4..8], .big);
const unit = chunk.data[8];
if (pixels_per_unit_x == 0 or pixels_per_unit_y == 0 or unit > 1) {
return error.InvalidPng;
}
return .{
.pixels_per_unit_x = pixels_per_unit_x,
.pixels_per_unit_y = pixels_per_unit_y,
.unit = @enumFromInt(unit),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const PhysicalUnit = enum(u8) {
unknown = 0,
meter = 1,
};
// --- tIME --------------------------------------------------------------------
pub const LastModificationTime = struct {
year: u16,
/// 1 - 12
month: u8,
/// 1 - 31
day: u8,
/// 0 - 23
hour: u8,
/// 0 - 59
minute: u8,
/// 0 - 60 (may include leap seconds)
second: u8,
pub fn decode(chunk: Chunk) !LastModificationTime {
std.debug.assert(chunk.chunk_type == .tIME);
if (chunk.data.len == 7) return error.InvalidPng;
const year = std.mem.readInt(u16, chunk.data[0..2], .big);
const month = chunk.data[2];
const day = chunk.data[3];
const hour = chunk.data[4];
const minute = chunk.data[5];
const second = chunk.data[6];
// NOTE This implementation is intentionally trying not to be too clever
// with validation.
if (month < 1 or month > 12 or
day < 1 or day > 31 or
hour > 23 or
minute > 59 or
second > 60)
{
return error.InvalidPng;
}
return .{
.year = year,
.month = month,
.day = day,
.hour = hour,
.minute = minute,
.second = second,
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
// -----------------------------------------------------------------------------
pub const ChunkType = enum(u32) {
IHDR = fromName("IHDR"),
PLTE = fromName("PLTE"),
IDAT = fromName("IDAT"),
IEND = fromName("IEND"),
tRNS = fromName("tRNS"),
gAMA = fromName("gAMA"),
cHRM = fromName("cHRM"),
sRGB = fromName("sRGB"),
iCCP = fromName("iCCP"),
tEXt = fromName("tEXt"),
zTXt = fromName("zTXt"),
iTXt = fromName("iTXt"),
bKGD = fromName("bKGD"),
pHYs = fromName("pHYs"),
sBIT = fromName("sBIT"),
sPLT = fromName("sPLT"),
hIST = fromName("hIST"),
tIME = fromName("tIME"),
_,
fn fromName(name: *const [4]u8) u32 {
return @bitCast(name.*);
}
pub fn fromBytes(bytes: *const [4]u8) ChunkType {
return @enumFromInt(fromName(bytes));
}
pub fn toBytes(self: ChunkType) [4]u8 {
return @bitCast(@intFromEnum(self));
}
pub fn ancillary(self: ChunkType) bool {
return self.toBytes()[0] & 0b0010_0000 != 0;
}
pub fn private(self: ChunkType) bool {
return self.toBytes()[1] & 0b0010_0000 != 0;
}
pub fn safeToCopy(self: ChunkType) bool {
return self.toBytes()[3] & 0b0010_0000 != 0;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
pub const Chunk = struct {
chunk_type: ChunkType,
data: []const u8,
};
pub const StandardKeyword = enum {
/// Short (one line) title or caption for image
Title,
/// Name of images creator
Author,
/// Description of image (possibly long)
Description,
/// Copyright notice
Copyright,
/// Time Time of original image creation
Creation,
/// Software used to create the image
Software,
/// Legal disclaimer
Disclaimer,
/// Warning of nature of content
Warning,
/// Device used to create the image
Source,
/// Miscellaneous comment; conversion from GIF comment
Comment,
pub const map: std.StaticStringMap(StandardKeyword) = blk: {
const fields = @typeInfo(StandardKeyword).@"enum".fields;
var kvs_list: [fields.len]struct { []const u8, StandardKeyword } = undefined;
for (fields, 0..) |field, i| {
kvs_list[i] = .{ field.name, @field(StandardKeyword, field.name) };
}
break :blk .initComptime(kvs_list);
};
pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword {
return map.get(keyword);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
/// The caller asserts that the buffer is at least `format.magic_length` bytes
/// long.
pub fn isPng(buffer: []const u8) bool {
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
}
/// The caller asserts that the buffer is at least `format.info_length` bytes
/// long.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= format.info_length);
if (!isPng(buffer)) {
return null;
}
const chunk, _ = (decodeChunk(buffer[format.magic_length..]) catch return null) orelse return null;
if (chunk.chunk_type != .IHDR) {
return null;
}
const header = Header.decode(chunk) catch return null;
return header;
}
pub fn decodeChunk(buffer: []const u8) !?struct { Chunk, []const u8 } {
var rest: []const u8 = buffer;
if (rest.len < 8) {
// Not enough data
return null;
}
const length = std.mem.readInt(u32, rest[0..4], .big);
const chunk_type: ChunkType = .fromBytes(rest[4..8]);
rest = rest[8..];
if (length > 0x7FFF_FFFF) {
return error.InvalidPng;
}
if (rest.len < length) {
// Not enough data
return null;
}
const data = rest[0..length];
rest = rest[length..];
if (rest.len < 4) {
// Not enough data
return null;
}
const crc = std.mem.readInt(u32, rest[0..4], .big);
rest = rest[4..];
if (std.hash.crc.Crc32IsoHdlc.hash(data) != crc) {
return error.InvalidPng;
}
return .{
.{ .chunk_type = chunk_type, .data = data },
rest,
};
}
pub fn decodeChunks(buffer: []const u8, chunks: ?[]Chunk) !usize {
if (buffer.len < format.magic_length or !isPng(buffer)) {
return error.InvalidPng;
}
var index: usize = 0;
var rest: []const u8 = buffer[format.magic_length..];
while (rest.len > 0) : (index += 1) {
const chunk, rest = try decodeChunk(rest) orelse return error.InvalidPng;
if (chunks) |ck| {
if (index < ck.len) {
ck[index] = chunk;
}
}
}
return index;
}
pub fn decodeChunksAlloc(buffer: []const u8, allocator: std.mem.Allocator) ![]Chunk {
const n = try decodeChunks(buffer, null);
const chunks = try allocator.alloc(Chunk, n);
errdefer allocator.free(chunks);
_ = try decodeChunks(buffer, chunks);
return chunks;
}
pub fn encodeChunks(chunks: []const Chunk, writer: *std.Io.Writer) !void {
try writer.writeAll(magic);
for (chunks) |chunk| {
var crc: std.hash.crc.Crc32IsoHdlc = .init();
var data: []const u8 = chunk.data;
while (data.len > 0) {
const bytes_written = try writer.write(data);
crc.update(data[0..bytes_written]);
data = data[bytes_written..];
}
try writer.writeInt(u32, crc.final(), .big);
}
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -1,8 +1,31 @@
const std = @import("std");
const Format = @import("Format.zig");
const format: Format = .{
.magic_length = magic.len,
// 4B - sample count
// 1B - channels
// 3B - sample rate
.info_length = magic.len + 8,
.extension = "qoa",
.media_type = "audio/qoa",
.isFormat = isQoa,
};
const magic = "qoaf";
const Header = union(enum) {
streaming: HeaderStreaming,
static: HeaderStatic,
pub fn initStatic(static: HeaderStatic) Header {
return .{ .static = static };
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
const HeaderStreaming = void;
@@ -17,32 +40,48 @@ const HeaderStatic = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / sample_rate);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
/// The caller asserts that the buffer is at least 12 bytes long, which can
/// contain the entirety of a QOA file header and the relevant information in
/// the first frame header.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= 12);
/// The caller asserts that the buffer is at least `format.magic_length` bytes
/// long.
pub fn isQoa(buffer: []const u8) bool {
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
}
/// The caller asserts that the buffer is at least `format.info_length` bytes
/// long.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= format.info_length);
if (!isQoa(buffer)) {
return null;
}
const magic = buffer[0..4];
const samples = std.mem.readInt(u32, buffer[4..8], .big);
const channels = buffer[8];
const sample_rate = std.mem.readInt(u24, buffer[9..12], .big);
if (!std.mem.eql(u8, magic, "qoaf") or channels == 0 or channels > 8 or sample_rate == 0) {
if (channels == 0 or channels > 8 or
sample_rate == 0)
{
return null;
}
if (samples == 0) {
return .streaming;
} else {
return .{
.static = .{
.samples = samples,
.channels = channels,
.sample_rate = sample_rate,
},
};
return .initStatic(.{
.samples = samples,
.channels = channels,
.sample_rate = sample_rate,
});
}
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -1,5 +1,21 @@
const std = @import("std");
const Format = @import("Format.zig");
const format: Format = .{
.magic_length = magic.len,
// 4B - width
// 4B - height
// 1B - channels
// 1B - color space
.info_length = magic.len + 10,
.extension = "qoi",
.media_type = "image/qoi",
.isFormat = isQoi,
};
const magic = "qoif";
const Channels = enum(u8) {
rgb = 3,
rgba = 4,
@@ -17,18 +33,31 @@ const Header = struct {
color_space: ColorSpace,
};
/// The caller asserts that the buffer is at least 14 bytes long, which can
/// contain the entirety of a QOI header.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= 14);
/// The caller asserts that the buffer is at least `format.magic_length` bytes
/// long.
pub fn isQoi(buffer: []const u8) bool {
return std.mem.eql(u8, buffer[0..format.magic_length], magic);
}
/// The caller asserts that the buffer is at least `format.info_length` bytes
/// long.
pub fn info(buffer: []const u8) ?Header {
std.debug.assert(buffer.len >= format.info_length);
if (!isQoi(buffer)) {
return null;
}
const magic = buffer[0..4];
const width = std.mem.readInt(u32, buffer[4..8], .big);
const height = std.mem.readInt(u32, buffer[8..12], .big);
const channels = buffer[12];
const color_space = buffer[13];
if (!std.mem.eql(u8, magic, "qoif") or width == 0 or height == 0 or channels < 3 or channels > 4 or color_space > 1) {
if (width == 0 or
height == 0 or
channels < 3 or channels > 4 or
color_space > 1)
{
return null;
}
@@ -39,3 +68,7 @@ pub fn info(buffer: []const u8) ?Header {
.color_space = @enumFromInt(color_space),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -1,4 +1,15 @@
const std = @import("std");
pub const audio = @import("audio.zig");
pub const Format = @import("Format.zig");
pub const image = @import("image.zig");
pub const jpeg = @import("jpeg.zig");
pub const jxl = @import("jxl.zig");
pub const png = @import("png.zig");
pub const qoa = @import("qoa.zig");
pub const qoi = @import("qoi.zig");
pub const stbi = @import("stbi.zig");
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

310
packages/media/src/stbi.zig Normal file
View File

@@ -0,0 +1,310 @@
const std = @import("std");
const Self = @This();
const image = @import("image.zig");
const vm = @import("vecmath");
allocator: std.mem.Allocator,
io: std.Io,
mutex: std.Io.Mutex = .init,
allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty,
allocated_bytes: usize = 0,
const alignment: std.mem.Alignment = .@"16";
const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque;
const log = std.log.scoped(.stbi);
pub fn init(allocator: std.mem.Allocator, io: std.Io) Self {
return .{
.allocator = allocator,
.io = io,
};
}
pub fn deinit(self: *Self) void {
std.log.scoped(.deinit).debug("Deinitializing {*}", .{self});
if (self.allocated_bytes > 0) {
log.warn("{d} byte(s) still allocated while deinitializing", .{self.allocated_bytes});
}
if (self.allocations.size > 0) {
log.warn("{d} allocation(s) still tracked while deinitializing", .{self.allocations.size});
var it = self.allocations.iterator();
var index: usize = 0;
while (it.next()) |entry| : (index += 1) {
log.warn("Leaked allocation ({d}/{d}) at 0x{x} of {d} byte(s)", .{
index + 1,
self.allocations.size,
@intFromPtr(entry.key_ptr.*),
entry.value_ptr.*,
});
const memory = @as([*]align(alignment.toByteUnits()) u8, @ptrCast(@alignCast(entry.key_ptr.*)))[0..entry.value_ptr.*];
self.allocator.free(memory);
}
}
self.allocations.deinit(self.allocator);
}
const import = struct {
extern fn stbi_load_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) u8;
extern fn stbi_load_16_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) u16;
extern fn stbi_loadf_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) f32;
extern fn stbi_load_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) u8;
extern fn stbi_load_16_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) u16;
extern fn stbi_loadf_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque, x: ?*i32, y: ?*i32, channels_in_file: ?*i32, desired_channels: i32) ?[*]align(alignment.toByteUnits()) f32;
extern fn stbi_info_from_memory(buffer: [*]const u8, len: i32, x: ?*i32, y: ?*i32, channels_in_file: ?*i32) i32;
extern fn stbi_is_16_bit_from_memory(buffer: [*]const u8, len: i32) i32;
extern fn stbi_is_hdr_from_memory(buffer: [*]const u8, len: i32) i32;
extern fn stbi_info_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque, x: ?*i32, y: ?*i32, channels_in_file: ?*i32) i32;
extern fn stbi_is_16_bit_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque) i32;
extern fn stbi_is_hdr_from_callbacks(callbacks: *const IoCallbacks, ctx: ?*anyopaque) i32;
};
pub fn loadStaticBuf(self: *Self, comptime W: u32, comptime H: u32, buf: []const u8) !image.Static(W, H) {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res = import.stbi_load_from_memory(buf.ptr, @intCast(buf.len), &x, &y, null, 4) orelse return error.StbiError;
defer castle_media_stbi_free(res);
if (x != W or y != H) return error.WrongDimensions;
return .{ .data = @as(*const [W * H]vm.Color, @ptrCast(@alignCast(res))).* };
}
pub fn loadStaticIo(self: *Self, comptime W: u32, comptime H: u32, reader: *std.Io.Reader) !image.Static(W, H) {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res = import.stbi_load_from_callbacks(&.std_io_reader_interface, reader, &x, &y, null, 4) orelse return error.StbiError;
defer castle_media_stbi_free(res);
if (x != W or y != H) return error.WrongDimensions;
return .{ .data = @as(*const [W * H]vm.Color, @ptrCast(@alignCast(res))).* };
}
/// On success, must free memory by calling `freeDynamic` method.
pub fn loadDynamicBuf(self: *Self, buf: []const u8) !image.Dynamic {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res = import.stbi_load_from_memory(buf.ptr, @intCast(buf.len), &x, &y, null, 4) orelse return error.StbiError;
const buffer_ptr: [*]vm.Color = @ptrCast(@alignCast(res));
const ux: u32 = @intCast(x);
const uy: u32 = @intCast(y);
return .initBuffer(ux, uy, buffer_ptr[0 .. ux * uy]);
}
/// On success, must free memory by calling `freeDynamic` method.
pub fn loadDynamicIo(self: *Self, reader: *std.Io.Reader) !image.Dynamic {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res = import.stbi_load_from_callbacks(&.std_io_reader_interface, reader, &x, &y, null, 4) orelse return error.StbiError;
const buffer_ptr: [*]vm.Color = @ptrCast(@alignCast(res));
const ux: u32 = @intCast(x);
const uy: u32 = @intCast(y);
return .initBuffer(ux, uy, buffer_ptr[0 .. ux * uy]);
}
/// On success, must free memory by calling `freeHdr` method.
pub fn loadHdrBuf(self: *Self, buf: []const u8) !image.Hdr {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res_f32 = import.stbi_loadf_from_memory(buf.ptr, @intCast(buf.len), &x, &y, null, 4) orelse return error.StbiError;
defer castle_media_stbi_free(res_f32);
const ux: u32 = @intCast(x);
const uy: u32 = @intCast(y);
const buffer_ptr_f32: [*]vm.Vector4 = @ptrCast(res_f32);
const buffer_ptr_f16: [*]vm.ColorHdr = @ptrCast(castle_media_stbi_malloc(ux * uy * @sizeOf(vm.ColorHdr)) orelse return error.OutOfMemory);
errdefer castle_media_stbi_free(buffer_ptr_f16);
for (buffer_ptr_f16[0 .. ux * uy], buffer_ptr_f32[0 .. ux * uy]) |*sample_f16, sample_f32| {
sample_f16.* = .init(
std.math.clamp(@as(f16, @floatCast(sample_f32.x)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.y)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.z)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.w)), -std.math.floatMax(f16), std.math.floatMax(f16)),
);
}
return .initBuffer(ux, uy, buffer_ptr_f16[0 .. ux * uy]);
}
/// On success, must free memory by calling `freeHdr` method.
pub fn loadHdrIo(self: *Self, reader: *std.Io.Reader) !image.Hdr {
current_self = self;
defer current_self = undefined;
var x: i32 = undefined;
var y: i32 = undefined;
const res_f32 = import.stbi_loadf_from_callbacks(&.std_io_reader_interface, reader, &x, &y, null, 4) orelse return error.StbiError;
defer castle_media_stbi_free(res_f32);
const ux: u32 = @intCast(x);
const uy: u32 = @intCast(y);
const buffer_ptr_f32: [*]vm.Vector4 = @ptrCast(res_f32);
const buffer_ptr_f16: [*]vm.ColorHdr = @ptrCast(castle_media_stbi_malloc(ux * uy * @sizeOf(vm.ColorHdr)) orelse return error.OutOfMemory);
errdefer castle_media_stbi_free(buffer_ptr_f16);
for (buffer_ptr_f16[0 .. ux * uy], buffer_ptr_f32[0 .. ux * uy]) |*sample_f16, sample_f32| {
sample_f16.* = .init(
std.math.clamp(@as(f16, @floatCast(sample_f32.x)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.y)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.z)), -std.math.floatMax(f16), std.math.floatMax(f16)),
std.math.clamp(@as(f16, @floatCast(sample_f32.w)), -std.math.floatMax(f16), std.math.floatMax(f16)),
);
}
return .initBuffer(ux, uy, buffer_ptr_f16[0 .. ux * uy]);
}
pub fn freeDynamic(self: *Self, img: image.Dynamic) void {
current_self = self;
defer current_self = undefined;
castle_media_stbi_free(@ptrCast(@alignCast(img.data)));
}
pub fn freeHdr(self: *Self, img: image.Hdr) void {
current_self = self;
defer current_self = undefined;
castle_media_stbi_free(@ptrCast(@alignCast(img.data)));
}
// --- IO INTERFACE ------------------------------------------------------------
pub const IoCallbacks = extern struct {
/// Fill `data` with `size` bytes. Return number of bytes actually read.
read: ?*const fn (ctx: ?*anyopaque, data: [*]u8, size: i32) callconv(.c) i32,
/// Skip the next `n` bytes, or backtrack `-n` bytes if `n < 0`.
skip: ?*const fn (ctx: ?*anyopaque, n: i32) callconv(.c) i32,
/// Return non-zero value if at the end of file/data.
eof: ?*const fn (cxt: ?*anyopaque) callconv(.c) i32,
pub const std_io_reader_interface: IoCallbacks = .{
.read = stdIoReader_ReadFn,
.skip = stdIoReader_SkipFn,
.eof = stdIoReader_EofFn,
};
pub fn stdIoReader_ReadFn(ctx: ?*anyopaque, data: [*]u8, size: i32) callconv(.c) i32 {
const reader: *std.Io.Reader = @ptrCast(@alignCast(ctx.?));
const bytes_read = reader.readSliceShort(data[0..@intCast(size)]) catch return 0;
return @intCast(bytes_read);
}
pub fn stdIoReader_SkipFn(ctx: ?*anyopaque, n: i32) callconv(.c) i32 {
const reader: *std.Io.Reader = @ptrCast(@alignCast(ctx.?));
// NOTE stb_image.h actually discards the return value from this
// callback. If an actual error occurs, we're cooked (but it will be
// very likely caught as a parsing error later).
_ = reader.discardAll(@intCast(n)) catch return 0;
return 0;
}
pub fn stdIoReader_EofFn(ctx: ?*anyopaque) callconv(.c) i32 {
const reader: *std.Io.Reader = @ptrCast(@alignCast(ctx.?));
_ = reader.peekByte() catch return 1;
return 0;
}
};
// --- MALLOC INTERFACE --------------------------------------------------------
threadlocal var current_self: *Self = undefined;
export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
const self = current_self;
self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(self.io);
self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null;
const memory = self.allocator.alignedAlloc(u8, alignment, size) catch return null;
self.allocations.putAssumeCapacityNoClobber(memory.ptr, size);
self.allocated_bytes += size;
//log.debug("Allocated {d} bytes(s) at 0x{x}", .{ size, @intFromPtr(memory.ptr) });
return memory.ptr;
}
export fn castle_media_stbi_realloc(maybe_ptr: VoidPtr, size: usize) callconv(.c) VoidPtr {
const self = current_self;
self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(self.io);
// NOTE If we were pedantic, we would consider the fact that we might not
// need unused capacity if the memory doesn't get relocated.
self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null;
const old_memory = if (maybe_ptr) |ptr| blk_then: {
const old_size = self.allocations.get(ptr).?;
break :blk_then @as([*]align(alignment.toByteUnits()) u8, @ptrCast(ptr))[0..old_size];
} else blk_else: {
break :blk_else @as([]align(alignment.toByteUnits()) u8, &.{});
};
const memory = self.allocator.realloc(old_memory, size) catch return null;
if (maybe_ptr) |ptr| {
const old_size = self.allocations.fetchRemove(ptr).?.value;
self.allocated_bytes -= old_size;
}
self.allocations.putAssumeCapacityNoClobber(memory.ptr, size);
self.allocated_bytes += size;
//log.debug("Reallocated into {d} bytes(s) at 0x{x} from 0x{x}", .{ size, @intFromPtr(memory.ptr), @intFromPtr(maybe_ptr) });
return memory.ptr;
}
export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
const self = current_self;
if (maybe_ptr) |ptr| {
self.mutex.lockUncancelable(self.io);
defer self.mutex.unlock(self.io);
const size = self.allocations.fetchRemove(ptr).?.value;
self.allocated_bytes -= size;
const memory = @as([*]align(alignment.toByteUnits()) u8, @ptrCast(ptr))[0..size];
self.allocator.free(memory);
}
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -0,0 +1,17 @@
#include <stddef.h>
void *castle_media_stbi_malloc(size_t size);
void *castle_media_stbi_realloc(void *ptr, size_t size);
void castle_media_stbi_free(void *ptr);
#define STBI_MALLOC(size) castle_media_stbi_malloc(size)
#define STBI_REALLOC(ptr, size) castle_media_stbi_realloc(ptr, size)
#define STBI_FREE(ptr) castle_media_stbi_free(ptr)
#define STBI_NO_STDIO
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#define STBI_ONLY_HDR
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,19 @@ pub fn build(b: *std.Build) void {
const sqlite_mod = sqlite_dep.module("sqlite");
const web_dep = b.dependency("web", .{
.target = target,
.optimize = optimize,
});
const web_mod = web_dep.module("web");
const myid_mod = b.addModule("myid", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.imports = &.{
.{ .name = "sqlite", .module = sqlite_mod },
.{ .name = "web", .module = web_mod },
},
});

View File

@@ -13,5 +13,8 @@
.url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345",
.hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA",
},
.web = .{
.path = "../web",
},
},
}

259
packages/myid/src/data.zig Normal file
View File

@@ -0,0 +1,259 @@
const std = @import("std");
const sqlite = @import("sqlite");
const web = @import("web");
const id = @import("id.zig");
pub const callback_regex = "^https?://([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}/";
pub const App = struct {
aid: id.App,
name: []const u8,
callbacks: []const []const u8,
};
pub const AppWithSecret = struct {
aid: id.App,
name: []const u8,
callbacks: []const []const u8,
secret: []const u8,
};
pub const CreateAppData = struct {
name: []const u8,
callback: ?[]const []const u8,
};
pub const CreateAppResult = struct {
aid: id.App,
plainSecret: []const u8,
};
pub const User = struct {
uid: id.User,
name: []const u8,
email: []const u8,
admin: bool,
};
pub const UserWithPassword = struct {
uid: id.User,
name: []const u8,
email: []const u8,
admin: bool,
passworD: []const u8,
};
pub const InviteUserData = struct {
name: []const u8,
email: []const u8,
admin: bool,
};
pub const CreateUserData = struct {
name: []const u8,
email: []const u8,
plainPassword: []const u8,
admin: bool,
};
pub const All = struct {
users: []const User,
apps: []const App,
};
pub const Database = struct {
db: sqlite.Db,
allocator: std.mem.Allocator,
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
var db = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{
.create = true,
.write = true,
},
.threading_mode = .MultiThread,
});
errdefer db.deinit();
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
return .{
.db = db,
.allocator = allocator,
};
}
pub fn deinit(self: *Database) void {
self.db.deinit();
self.* = undefined;
}
// --- MIGRATION -----------------------------------------------------------
fn getUserVersion(self: *Database) !i32 {
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
return version.?;
}
fn setUserVersion(self: *Database, version: i32) !void {
var buf: [100]u8 = undefined;
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
_ = try self.db.oneDynamic(void, query, .{}, .{});
}
pub fn migrate(self: *Database) !void {
var user_version = try self.getUserVersion();
if (user_version == 0) {
_ = try self.db.exec(
\\CREATE TABLE users (
\\ uid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ email TEXT NOT NULL UNIQUE,
\\ password TEXT NOT NULL,
\\ PRIMARY KEY (uid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE apps (
\\ aid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ secret TEXT NOT NULL,
\\ PRIMARY KEY (aid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE app_callbacks (
\\ aid BLOB NOT NULL,
\\ callback TEXT NOT NULL,
\\ PRIMARY KEY (aid, callback),
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
\\ ON UPDATE CASCADE
\\ ON DELETE CASCADE
\\)
, .{}, .{});
user_version += 1;
try self.setUserVersion(user_version);
}
}
// --- USERS ---------------------------------------------------------------
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !id.User {
const uid = id.User.init(web.UUID.v7());
var password_buf: [1000]u8 = undefined;
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &password_buf);
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
.uid = uid.bytes,
.name = name,
.email = email,
.password = password,
});
return uid;
}
// --- APPS ----------------------------------------------------------------
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
_ = try self.db.exec("BEGIN", .{}, .{});
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
const secret_bytes = blk: {
var bytes: [24]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var plain_secret_buf: [32]u8 = undefined;
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
std.debug.assert(plain_secret_buf.len == plain_secret.len);
const aid = id.App.init(web.UUID.v7());
var secret_buf: [1000]u8 = undefined;
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &secret_buf);
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
.aid = aid.bytes,
.name = name,
.secret = secret,
});
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
defer insert_callback.deinit();
for (callbacks) |callback| {
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
insert_callback.reset();
}
try self.db.exec("COMMIT", .{}, .{});
return .{
.aid = aid,
.plain_secret = plain_secret_buf,
};
}
};
test "user version" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try std.testing.expectEqual(0, try db.getUserVersion());
try db.setUserVersion(1);
try std.testing.expectEqual(1, try db.getUserVersion());
}
test "migrate" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
}
test "create user" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
const maybe_user = try db.db.oneAlloc(struct {
name: []const u8,
email: []const u8,
password: []const u8,
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
.uid = uid.bytes,
});
defer arena.deinit();
try std.testing.expect(maybe_user != null);
if (maybe_user) |user| {
try std.testing.expectEqualSlices(u8, "admin", user.name);
}
}
test "create app" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
_ = try db.createApp("app", &.{
"http://localhost:3000/callback",
"https://example.com/callback",
});
}

View File

@@ -1,2 +0,0 @@
pub const database_path = "db.sqlite3";
pub const socket_path = "myid.sock";

View File

@@ -1,206 +0,0 @@
const std = @import("std");
const main = @import("main.zig");
const Parser = @import("http/Parser.zig");
threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined;
threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined;
const log = std.log.scoped(.http);
const status = struct {
pub const ok = "HTTP/1.1 200 OK\r\n";
pub const created = "HTTP/1.1 201 Created\r\n";
pub const accepted = "HTTP/1.1 202 Accepted\r\n";
pub const non_authoritative_information = "HTTP/1.1 203 Non-Authoritative Information\r\n";
pub const no_content = "HTTP/1.1 204 No Content\r\n";
pub const reset_content = "HTTP/1.1 205 Reset Content\r\n";
pub const partial_content = "HTTP/1.1 206 Partial Content\r\n";
pub const multi_status = "HTTP/1.1 207 Multi-Status\r\n";
pub const already_reported = "HTTP/1.1 208 Already Reported\r\n";
pub const multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
pub const moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
pub const found = "HTTP/1.1 302 Found\r\n";
pub const see_other = "HTTP/1.1 303 See Other\r\n";
pub const not_modified = "HTTP/1.1 304 Not Modified\r\n";
pub const temporary_redirect = "HTTP/1.1 307 Temporary Redirect\r\n";
pub const permanent_redirect = "HTTP/1.1 308 Permanent Redirect\r\n";
pub const bad_request = "HTTP/1.1 400 Bad Request\r\n";
pub const unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
pub const payment_required = "HTTP/1.1 402 Payment Required\r\n";
pub const forbidden = "HTTP/1.1 403 Forbidden\r\n";
pub const not_found = "HTTP/1.1 404 Not Found\r\n";
pub const method_not_allowed = "HTTP/1.1 405 Method Not Allowed\r\n";
pub const not_acceptable = "HTTP/1.1 406 Not Acceptable\r\n";
pub const proxy_authentication_required = "HTTP/1.1 407 Proxy Authentication Required\r\n";
pub const request_timeout = "HTTP/1.1 408 Request Timeout\r\n";
pub const conflict = "HTTP/1.1 409 Conflict\r\n";
pub const gone = "HTTP/1.1 410 Gone\r\n";
pub const length_required = "HTTP/1.1 411 Length Required\r\n";
pub const precondition_failed = "HTTP/1.1 412 Precondition Failed\r\n";
pub const content_too_large = "HTTP/1.1 413 Content Too Large\r\n";
pub const uri_too_long = "HTTP/1.1 414 URI Too Long\r\n";
pub const unsupported_media_type = "HTTP/1.1 415 Unsupported Media Type\r\n";
pub const range_not_satisfiable = "HTTP/1.1 416 Range Not Satisfiable\r\n";
pub const expectation_failed = "HTTP/1.1 417 Expectation Failed\r\n";
pub const im_a_teapot = "HTTP/1.1 418 I'm a teapot\r\n";
pub const misdirected_request = "HTTP/1.1 421 Misdirected Request\r\n";
pub const unprocessable_content = "HTTP/1.1 422 Unprocessable Content\r\n";
pub const locked = "HTTP/1.1 423 Locked\r\n";
pub const failed_dependency = "HTTP/1.1 424 Failed Dependency\r\n";
pub const upgrade_required = "HTTP/1.1 426 Upgrade Required\r\n";
pub const precondition_required = "HTTP/1.1 428 Precondition Required\r\n";
pub const too_many_requests = "HTTP/1.1 429 Too Many Requests\r\n";
pub const request_header_fields_too_large = "HTTP/1.1 431 Request Header Fields Too Large\r\n";
pub const unavailable_for_legal_reasons = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n";
pub const internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n";
pub const not_implemented = "HTTP/1.1 501 Not Implemented\r\n";
pub const bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n";
pub const service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n";
pub const gateway_timeout = "HTTP/1.1 504 Gateway Timeout\r\n";
pub const http_version_not_supported = "HTTP/1.1 505 HTTP Version Not Supported\r\n";
pub const variant_also_negotiates = "HTTP/1.1 506 Variant Also Negotiates\r\n";
pub const insufficient_storage = "HTTP/1.1 507 Insufficient Storage\r\n";
pub const loop_detected = "HTTP/1.1 508 Loop Detected\r\n";
pub const not_extended = "HTTP/1.1 510 Not Extended\r\n";
pub const network_authentication_required = "HTTP/1.1 511 Network Authentication Required\r\n";
};
const ResponseEmptyOptions = struct {
status_text: []const u8 = status.ok,
};
const ResponseOptions = struct {
status_text: []const u8 = status.ok,
media_type: []const u8 = "text/plain; charset=utf-8",
response_body: []const u8,
};
fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("\r\n", .{});
return fbs.getWritten();
}
fn makeResponseClose(options: ResponseEmptyOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("Connection: close\r\n", .{});
try writer.print("\r\n", .{});
return fbs.getWritten();
}
fn makeResponse(options: ResponseOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("Content-Type: {s}\r\n", .{options.media_type});
try writer.print("Content-Length: {d}\r\n", .{options.response_body.len});
try writer.print("\r\n", .{});
try writer.print("{s}", .{options.response_body});
return fbs.getWritten();
}
pub fn process(conn: std.net.Server.Connection) !void {
defer conn.stream.close();
var leftover_bytes: usize = 0;
while (true) {
const start = try std.time.Instant.now();
var route: Parser.Route = undefined;
var parser = Parser.init(.{
.self = &route,
.route = routeCallback,
});
var total_bytes_read: usize = 0;
while (true) {
var bytes_read: usize = undefined;
var chars: []const u8 = undefined;
if (leftover_bytes > 0) {
bytes_read = leftover_bytes;
chars = read_buffer[0..leftover_bytes];
leftover_bytes = 0;
} else {
bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]);
chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read];
}
total_bytes_read += bytes_read;
const res = parser.consume(chars) catch |err| switch (err) {
error.MethodNotSupported => {
const response = try makeResponseClose(.{ .status_text = status.method_not_allowed });
try conn.stream.writeAll(response);
return;
},
error.HttpVersionNotSupported => {
const response = try makeResponseClose(.{ .status_text = status.http_version_not_supported });
try conn.stream.writeAll(response);
return;
},
error.MissingLineFeed => {
const response = try makeResponseClose(.{ .status_text = status.bad_request });
try conn.stream.writeAll(response);
return;
},
error.InvalidContentLength => {
const response = try makeResponseClose(.{ .status_text = status.bad_request });
try conn.stream.writeAll(response);
return;
},
};
if (total_bytes_read >= read_buffer.len and !res.done) {
if (parser.state == .body) {
const response = try makeResponseClose(.{ .status_text = status.content_too_large });
try conn.stream.writeAll(response);
return;
} else {
const response = try makeResponseClose(.{ .status_text = status.request_header_fields_too_large });
try conn.stream.writeAll(response);
return;
}
}
if (res.done) {
leftover_bytes = bytes_read - res.consumed;
break;
}
}
const response = try makeResponse(.{ .response_body = "PONG\n" });
try conn.stream.writeAll(response);
if (leftover_bytes > 0) {
@memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]);
}
const end = try std.time.Instant.now();
const time_ns = end.since(start);
const time_us = @divFloor(time_ns, std.time.ns_per_us);
log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us });
}
}
fn routeCallback(self: ?*anyopaque, route: Parser.Route) void {
@as(*Parser.Route, @alignCast(@ptrCast(self))).* = route;
}

View File

@@ -1,356 +0,0 @@
const std = @import("std");
const Parser = @This();
const Callbacks = struct {
self: ?*anyopaque = null,
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
pub const init: Callbacks = .{};
};
const Error = error{
MethodNotSupported,
HttpVersionNotSupported,
MissingLineFeed,
InvalidContentLength,
};
const State = union(enum) {
pub fn methodComplete(method: Method) State {
return .{
.method_complete = .{
.method = method,
},
};
}
pub fn pathname(method: Method, p: []const u8) State {
return .{
.pathname_state = .{
.method = method,
.pathname = p,
},
};
}
pub fn headerValue(name: []const u8, value: []const u8) State {
return .{
.header_value = .{
.name = name,
.value = value,
},
};
}
init: void,
method_d: void,
method_g: void,
method_h: void,
method_p: void,
method_de: void,
method_ge: void,
method_he: void,
method_pa: void,
method_po: void,
method_pu: void,
method_del: void,
method_hea: void,
method_pat: void,
method_pos: void,
method_dele: void,
method_patc: void,
method_delet: void,
method_complete: struct { method: Method },
pathname_state: struct { method: Method, pathname: []const u8 },
pathname_complete: void,
version_h: void,
version_ht: void,
version_htt: void,
version_http: void,
@"version_http/@": void,
@"version_http/1@": void,
@"version_http/1.@": void,
version_complete: void,
start_line_end: void,
header_name_start: void,
header_name: []const u8,
header_value: struct { name: []const u8, value: []const u8 },
header_line_end: void,
headers_end: void,
body: []const u8,
};
const ConsumeResult = struct {
consumed: usize,
done: bool,
};
const ConsumeCharResult = enum {
not_done,
done,
};
pub const Method = enum {
DELETE,
GET,
HEAD,
PATCH,
POST,
PUT,
};
pub const Route = struct {
method: Method,
pathname: []const u8,
};
callbacks: Callbacks,
state: State,
current_header_is_content_length: bool,
content_length: usize,
pub fn init(callbacks: Callbacks) Parser {
return .{
.callbacks = callbacks,
.state = .init,
.current_header_is_content_length = false,
.content_length = 0,
};
}
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
var i: usize = 0;
while (i < chars.len) {
switch (self.state) {
.body => |body| {
const to_consume = @min(chars.len - i, self.content_length - body.len);
const new_body = body.ptr[0 .. body.len + to_consume];
self.state = .{ .body = new_body };
i += to_consume;
const done = new_body.len >= self.content_length;
if (done) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
}
return .{
.consumed = i,
.done = done,
};
},
else => {
const res = try self.consumeChar(&chars[i]);
i += 1;
if (res == .done) return .{
.consumed = i,
.done = true,
};
},
}
}
return .{
.consumed = chars.len,
.done = false,
};
}
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
const c = c_ptr.*;
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
switch (self.state) {
.init => switch (c) {
'D' => self.state = .method_d,
'G' => self.state = .method_g,
'H' => self.state = .method_h,
'P' => self.state = .method_p,
else => return error.MethodNotSupported,
},
.method_d => switch (c) {
'E' => self.state = .method_de,
else => return error.MethodNotSupported,
},
.method_g => switch (c) {
'E' => self.state = .method_ge,
else => return error.MethodNotSupported,
},
.method_h => switch (c) {
'E' => self.state = .method_he,
else => return error.MethodNotSupported,
},
.method_p => switch (c) {
'A' => self.state = .method_pa,
'O' => self.state = .method_po,
'U' => self.state = .method_pu,
else => return error.MethodNotSupported,
},
.method_de => switch (c) {
'L' => self.state = .method_del,
else => return error.MethodNotSupported,
},
.method_ge => switch (c) {
'T' => self.state = .methodComplete(.GET),
else => return error.MethodNotSupported,
},
.method_he => switch (c) {
'A' => self.state = .method_hea,
else => return error.MethodNotSupported,
},
.method_pa => switch (c) {
'T' => self.state = .method_pat,
else => return error.MethodNotSupported,
},
.method_po => switch (c) {
'S' => self.state = .method_pos,
else => return error.MethodNotSupported,
},
.method_pu => switch (c) {
'T' => self.state = .methodComplete(.PUT),
else => return error.MethodNotSupported,
},
.method_del => switch (c) {
'E' => self.state = .method_dele,
else => return error.MethodNotSupported,
},
.method_hea => switch (c) {
'D' => self.state = .methodComplete(.HEAD),
else => return error.MethodNotSupported,
},
.method_pat => switch (c) {
'C' => self.state = .method_patc,
else => return error.MethodNotSupported,
},
.method_pos => switch (c) {
'T' => self.state = .methodComplete(.POST),
else => return error.MethodNotSupported,
},
.method_dele => switch (c) {
'T' => self.state = .method_delet,
else => return error.MethodNotSupported,
},
.method_patc => switch (c) {
'H' => self.state = .methodComplete(.PATCH),
else => return error.MethodNotSupported,
},
.method_delet => switch (c) {
'E' => self.state = .methodComplete(.DELETE),
else => return error.MethodNotSupported,
},
.method_complete => |s| switch (c) {
' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]),
else => return error.MethodNotSupported,
},
.pathname_state => |s| switch (c) {
' ' => {
self.state = .pathname_complete;
if (self.callbacks.route) |routeCallback| {
routeCallback(self.callbacks.self, .{
.method = s.method,
.pathname = s.pathname,
});
}
},
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
},
.pathname_complete => switch (c) {
'H' => self.state = .version_h,
else => return error.HttpVersionNotSupported,
},
.version_h => switch (c) {
'T' => self.state = .version_ht,
else => return error.HttpVersionNotSupported,
},
.version_ht => switch (c) {
'T' => self.state = .version_htt,
else => return error.HttpVersionNotSupported,
},
.version_htt => switch (c) {
'P' => self.state = .version_http,
else => return error.HttpVersionNotSupported,
},
.version_http => switch (c) {
'/' => self.state = .@"version_http/@",
else => return error.HttpVersionNotSupported,
},
.@"version_http/@" => switch (c) {
'1' => self.state = .@"version_http/1@",
else => return error.HttpVersionNotSupported,
},
.@"version_http/1@" => switch (c) {
'.' => self.state = .@"version_http/1.@",
else => return error.HttpVersionNotSupported,
},
.@"version_http/1.@" => switch (c) {
'1' => self.state = .version_complete,
else => return error.HttpVersionNotSupported,
},
.version_complete => switch (c) {
'\r' => self.state = .start_line_end,
else => return error.HttpVersionNotSupported,
},
.start_line_end => switch (c) {
'\n' => self.state = .header_name_start,
else => return error.MissingLineFeed,
},
.header_name_start => switch (c) {
'\r' => self.state = .headers_end,
else => self.state = .{ .header_name = c_slice },
},
.header_name => |name| switch (c) {
':' => {
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
},
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
},
.header_value => |s| switch (c) {
'\r' => {
self.state = .header_line_end;
const value_trimmed = std.mem.trim(u8, s.value, " \t");
if (self.current_header_is_content_length) {
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
self.current_header_is_content_length = false;
}
if (self.callbacks.header) |headerCallback| {
headerCallback(self.callbacks.self, s.name, value_trimmed);
}
},
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
},
.header_line_end => switch (c) {
'\n' => self.state = .header_name_start,
else => return error.MissingLineFeed,
},
.headers_end => switch (c) {
'\n' => {
if (self.content_length == 0) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, &.{});
}
return .done;
}
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
},
else => return error.MissingLineFeed,
},
.body => |body| {
const new_body = body.ptr[0 .. body.len + 1];
self.state = .{ .body = new_body };
if (new_body.len >= self.content_length) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
return .done;
}
},
}
return .not_done;
}

6
packages/myid/src/id.zig Normal file
View File

@@ -0,0 +1,6 @@
const std = @import("std");
const web = @import("web");
pub const App = web.Id(.app);
pub const Session = web.Id(.session);
pub const User = web.Id(.user);

View File

@@ -1,235 +1,3 @@
const std = @import("std");
const sqlite = @import("sqlite");
const uuid = @import("uuid.zig");
fn Id(comptime _tag: @Type(.enum_literal)) type {
return struct {
pub const tag = _tag;
bytes: [16]u8,
pub fn new() @This() {
return .{ .bytes = uuid.uuid_v7() };
}
pub fn eql(a: @This(), b: @This()) bool {
return std.mem.eql(u8, &a.bytes, &b.bytes);
}
pub fn decode(encoded: *const [22]u8) !@This() {
var bytes: [16]u8 = undefined;
try std.base64.url_safe_no_pad.Decoder.decode(&bytes, encoded);
return .{ .bytes = bytes };
}
pub fn encode(self: @This()) [22]u8 {
var text: [22]u8 = undefined;
std.base64.url_safe_no_pad.Encoder.encode(&text, self.bytes);
return text;
}
};
}
pub const AppId = Id(.app_id);
pub const UserId = Id(.user_id);
pub const CreateAppResult = struct {
aid: AppId,
plain_secret: [32]u8,
};
pub const Database = struct {
db: sqlite.Db,
allocator: std.mem.Allocator,
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
var db = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{
.create = true,
.write = true,
},
.threading_mode = .MultiThread,
});
errdefer db.deinit();
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
return .{
.db = db,
.allocator = allocator,
};
}
pub fn deinit(self: *Database) void {
self.db.deinit();
self.* = undefined;
}
// --- MIGRATION -----------------------------------------------------------
fn getUserVersion(self: *Database) !i32 {
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
return version.?;
}
fn setUserVersion(self: *Database, version: i32) !void {
var buf: [100]u8 = undefined;
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
_ = try self.db.oneDynamic(void, query, .{}, .{});
}
pub fn migrate(self: *Database) !void {
var user_version = try self.getUserVersion();
if (user_version == 0) {
_ = try self.db.exec(
\\CREATE TABLE users (
\\ uid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ email TEXT NOT NULL UNIQUE,
\\ password TEXT NOT NULL,
\\ PRIMARY KEY (uid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE apps (
\\ aid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ secret TEXT NOT NULL,
\\ PRIMARY KEY (aid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE app_callbacks (
\\ aid BLOB NOT NULL,
\\ callback TEXT NOT NULL,
\\ PRIMARY KEY (aid, callback),
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
\\ ON UPDATE CASCADE
\\ ON DELETE CASCADE
\\)
, .{}, .{});
user_version += 1;
try self.setUserVersion(user_version);
}
}
// --- USERS ---------------------------------------------------------------
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !UserId {
const uid = UserId.new();
var password_buf: [1000]u8 = undefined;
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &password_buf);
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
.uid = uid.bytes,
.name = name,
.email = email,
.password = password,
});
return uid;
}
// --- APPS ----------------------------------------------------------------
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
_ = try self.db.exec("BEGIN", .{}, .{});
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
const secret_bytes = blk: {
var bytes: [24]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var plain_secret_buf: [32]u8 = undefined;
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
std.debug.assert(plain_secret_buf.len == plain_secret.len);
const aid = AppId.new();
var secret_buf: [1000]u8 = undefined;
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &secret_buf);
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
.aid = aid.bytes,
.name = name,
.secret = secret,
});
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
defer insert_callback.deinit();
for (callbacks) |callback| {
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
insert_callback.reset();
}
try self.db.exec("COMMIT", .{}, .{});
return .{
.aid = aid,
.plain_secret = plain_secret_buf,
};
}
};
test "user version" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try std.testing.expectEqual(0, try db.getUserVersion());
try db.setUserVersion(1);
try std.testing.expectEqual(1, try db.getUserVersion());
}
test "migrate" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
}
test "create user" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
const maybe_user = try db.db.oneAlloc(struct {
name: []const u8,
email: []const u8,
password: []const u8,
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
.uid = uid.bytes,
});
defer arena.deinit();
try std.testing.expect(maybe_user != null);
if (maybe_user) |user| {
try std.testing.expectEqualSlices(u8, "admin", user.name);
}
}
test "create app" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
_ = try db.createApp("app", &.{
"http://localhost:3000/callback",
"https://example.com/callback",
});
}
const web = @import("web");

View File

@@ -1,41 +0,0 @@
const std = @import("std");
var lock: std.Thread.Mutex = .{};
var last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
var counter: std.atomic.Value(u32) = .{ .raw = 0 };
fn getCount(timestamp: u64) u32 {
lock.lock();
defer lock.unlock();
if (last_timestamp.swap(timestamp, .monotonic) != timestamp) {
counter.store(0, .monotonic);
}
return counter.fetchAdd(1, .monotonic) % 4096;
}
pub fn uuid_v7() [16]u8 {
const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp()));
const count = getCount(timestamp);
const random = blk: {
var bytes: [8]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var res: [16]u8 = undefined;
res[0] = @truncate(timestamp >> 40);
res[1] = @truncate(timestamp >> 32);
res[2] = @truncate(timestamp >> 24);
res[3] = @truncate(timestamp >> 16);
res[4] = @truncate(timestamp >> 8);
res[5] = @truncate(timestamp);
res[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
res[7] = @truncate(count);
res[8] = 0x80 | (random[0] & 0x3F);
@memcpy(res[9..16], random[1..8]);
return res;
}

View File

@@ -1,40 +0,0 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .windows,
.abi = .msvc,
});
const optimize = b.standardOptimizeOption(.{});
const zigwin32 = b.dependency("zigwin32", .{});
const zigwin32_mod = zigwin32.module("win32");
zigwin32_mod.resolved_target = target;
zigwin32_mod.optimize = optimize;
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("win32", zigwin32_mod);
const exe = b.addExecutable(.{
.name = "sciter",
.root_module = exe_mod,
});
b.installArtifact(exe);
b.getInstallStep().dependOn(&b.addInstallBinFile(b.path("vendor/sciter.dll"), "sciter.dll").step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}

View File

@@ -1,18 +0,0 @@
.{
.name = .sciter,
.version = "0.0.0",
.minimum_zig_version = "0.15.2",
.paths = .{
"src",
"vendor",
"build.zig",
"build.zig.zon",
},
.fingerprint = 0x51b124a630f074d7,
.dependencies = .{
.zigwin32 = .{
.url = "https://github.com/marlersoft/zigwin32/archive/5587b16fa040573846a6bf531301f6206d31a6bf.zip",
.hash = "zigwin32-25.0.28-preview-AAAAAICM5AMResOGQnQ85mfe60TTOQeMtt7GRATUOKoP",
},
},
}

View File

@@ -1,39 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sciter Demo</title>
<style>
html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
background-color: white;
color: black;
}
#hello {
width: 120px;
height: 32px;
margin: 1*;
padding: 0;
border: 1px solid black;
text-align: center;
line-height: 32px;
}
</style>
</head>
<body>
<p id="hello">Hello, World!</p>
</body>
</html>

View File

@@ -1,115 +0,0 @@
const sciter = @import("sciter.zig");
const std = @import("std");
const wam = @import("win32").ui.windows_and_messaging;
const win32 = @import("win32").foundation;
const L = std.unicode.utf8ToUtf16LeStringLiteral;
const WINAPI = @import("std").builtin.CallingConvention.winapi;
const html = @embedFile("index.html");
const min_track_size: win32.POINT = .{ .x = 640, .y = 480 };
var sciter_api: *sciter.API = undefined;
pub fn wWinMain(
hInstance: std.os.windows.HINSTANCE,
hPrevInstance: ?std.os.windows.HINSTANCE,
lpCmdLine: [*:0]u16,
nCmdShow: i32,
) i32 {
_ = hPrevInstance;
_ = lpCmdLine;
const sciter_module = std.os.windows.LoadLibraryW(L("sciter.dll")) catch |err| {
_ = wam.MessageBoxW(
null,
switch (err) {
error.FileNotFound => L("Couldn't find sciter.dll."),
else => L("An unknown error occured while trying to load sciter.dll."),
},
L("Critical error"),
wam.MB_ICONHAND,
);
std.posix.exit(1);
};
const sciter_api_fn: *const fn () ?*sciter.API = @ptrCast(std.os.windows.kernel32.GetProcAddress(sciter_module, "SciterAPI") orelse {
_ = wam.MessageBoxW(null, L("Couldn't load Sciter API."), L("Critical error"), wam.MB_ICONHAND);
std.posix.exit(1);
});
sciter_api = sciter_api_fn() orelse {
_ = wam.MessageBoxW(null, L("Couldn't load Sciter API."), L("Critical error"), wam.MB_ICONHAND);
std.posix.exit(1);
};
std.debug.print("Sciter API version is: 0x{X:0>8}\n", .{sciter_api.version});
const wc = std.mem.zeroInit(wam.WNDCLASSEXW, .{
.cbSize = @sizeOf(wam.WNDCLASSEXW),
.lpfnWndProc = &WindowProc,
.hInstance = hInstance,
.hIcon = wam.LoadIconW(null, wam.IDI_APPLICATION),
.hCursor = wam.LoadCursorW(null, wam.IDC_ARROW),
.lpszClassName = L("SCITER_WINDOW"),
});
const atom = wam.RegisterClassExW(&wc);
std.debug.assert(atom != 0);
const hwnd = wam.CreateWindowExW(
wam.WS_EX_APPWINDOW,
wc.lpszClassName,
L("Sciter Demo"),
wam.WS_OVERLAPPEDWINDOW,
-1,
-1,
-1,
-1,
null,
null,
wc.hInstance,
null,
) orelse {
_ = wam.MessageBoxW(null, L("Couldn't create window."), L("Critical error"), wam.MB_ICONHAND);
std.posix.exit(1);
};
_ = sciter_api.SciterLoadHtml(hwnd, html, html.len, L("/"));
_ = wam.ShowWindow(hwnd, @bitCast(nCmdShow));
var msg = std.mem.zeroes(wam.MSG);
while (wam.GetMessageW(&msg, null, 0, 0) > 0) {
_ = wam.TranslateMessage(&msg);
_ = wam.DispatchMessageW(&msg);
}
return @bitCast(@as(u32, @truncate(msg.wParam)));
}
fn WindowProc(
hwnd: win32.HWND,
uMsg: u32,
wParam: win32.WPARAM,
lParam: win32.LPARAM,
) callconv(WINAPI) win32.LRESULT {
switch (uMsg) {
wam.WM_DESTROY => {
wam.PostQuitMessage(0);
},
wam.WM_GETMINMAXINFO => {
const mmi: *wam.MINMAXINFO = @ptrFromInt(@as(usize, @bitCast(lParam)));
mmi.ptMinTrackSize = min_track_size;
},
else => {},
}
var handled: sciter.Bool = .FALSE;
const result = sciter_api.SciterProcND(hwnd, uMsg, wParam, lParam, &handled);
if (handled != .FALSE) {
return result;
}
return wam.DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

View File

@@ -1,641 +0,0 @@
pub const CStr = [*:0]const u8;
pub const CWStr = [*:0]const u16;
pub const Element = opaque {};
pub const HElement = *Element;
pub const Node = opaque {};
pub const HNode = *Node;
pub const SArchive = opaque {};
pub const HSArchive = *SArchive;
/// - Windows: `HWND`
/// - OS X: `NSView*`
/// - Linux/GTK: `GtkWidget*`
pub const HWindow = *anyopaque;
pub const BehaviorEventParams = extern struct {
cmd: BehaviorEvents,
he_target: ?HElement,
he: ?HElement,
reason: extern union {
click: enum(usize) { BY_MOUSE_CLICK, BY_KEY_CLICK, SYNTHESIZED, BY_MOUSE_ON_ICON },
edit_changed: enum(usize) { BY_INS_CHAR, BY_INS_CHARS, BY_DEL_CHAR, BY_DEL_CHARS, BY_UNDO_REDO },
custom: usize,
},
data: Value,
name: ?CWStr,
};
const BehaviorEvents = enum(u32) {
BUTTON_CLICK = 0,
BUTTON_PRESS = 1,
VALUE_CHANGED = 2,
VALUE_CHANGING = 3,
SELECTION_CHANGED = 5,
SELECTION_CHANGING = 0xC,
POPUP_REQUEST = 7,
POPUP_READY = 8,
POPUP_DISMISSED = 9,
MENU_ITEM_ACTIVE = 0xA,
MENU_ITEM_CLICK = 0xB,
CONTEXT_MENU_REQUEST = 0x10,
VISUAL_STATUS_CHANGED = 0x11,
DISABLED_STATUS_CHANGED = 0x12,
POPUP_DISMISSING = 0x13,
CONTENT_CHANGED = 0x15,
HYPERLINK_CLICK = 0x80,
ELEMENT_COLLAPSED = 0x90,
ELEMENT_EXPANDED = 0x91,
ACTIVATE_CHILD = 0x92,
FORM_SUBMIT = 0x96,
FORM_RESET = 0x97,
DOCUMENT_COMPLETE = 0x98,
HISTORY_PUSH = 0x99,
HISTORY_DROP = 0x9A,
HISTORY_PRIOR = 0x9B,
HISTORY_NEXT = 0x9C,
HISTORY_STATE_CHANGED = 0x9D,
CLOSE_POPUP = 0x9E,
REQUEST_TOOLTIP = 0x9F,
ANIMATION = 0xA0,
TRANSITION = 0xA1,
SWIPE = 0xB0,
DOCUMENT_CREATED = 0xC0,
DOCUMENT_CLOSE_REQUEST = 0xC1,
DOCUMENT_CLOSE = 0xC2,
DOCUMENT_READY = 0xC3,
DOCUMENT_PARSED = 0xC4,
//DOCUMENT_RELOAD = 0xC5,
DOCUMENT_CLOSING = 0xC6,
CONTAINER_CLOSE_REQUEST = 0xC7,
CONTAINER_CLOSING = 0xC8,
VIDEO_INITIALIZED = 0xD1,
VIDEO_STARTED = 0xD2,
VIDEO_STOPPED = 0xD3,
VIDEO_BIND_RQ = 0xD4,
VIDEO_FRAME_REQUEST = 0xD8,
PAGINATION_STARTS = 0xE0,
PAGINATION_PAGE = 0xE1,
PAGINATION_ENDS = 0xE2,
CUSTOM = 0xF0,
EGL_RENDER = 0x20,
/// All custom event codes shall be greater than this number. All codes
/// below this will be used solely by application - Sciter will not
/// intrepret it and will do just dispatching. To send event notifications
/// with these codes use SciterSend/PostEvent API.
FIRST_APPLICATION_EVENT_CODE = 0x100,
_,
};
pub const Bool = enum(u32) {
FALSE = 0,
TRUE = 1,
_,
};
pub const DomResult = enum(i32) {
OK = 0,
INVALID_HWND = 1,
INVALID_HANDLE = 2,
PASSIVE_HANDLE = 3,
INVALID_PARAMETER = 4,
OPERATION_FAILED = 5,
OK_NOT_HANDLED = -1,
};
pub const CtlType = enum(u32) {
NO = 0,
UNKNOWN = 1,
EDIT = 2,
NUMERIC = 3,
CLICKABLE = 4,
BUTTON = 5,
CHECKBOX = 6,
RADIO = 7,
SELECT_SINGLE = 8,
SELECT_MULTIPLE = 9,
DD_SELECT = 10,
TEXTAREA = 11,
HTMLAREA = 12,
PASSWORD = 13,
PROGRESS = 14,
SLIDER = 15,
DECIMAL = 16,
CURRENCY = 17,
SCROLLBAR = 18,
LIST = 19,
RICHTEXT = 20,
CALENDAR = 21,
DATE = 22,
TIME = 23,
FILE = 24,
PATH = 25,
HYPERLINK = 26,
FORM = 27,
MENUBAR = 28,
MENU = 29,
MENUBUTTON = 30,
FRAME = 31,
FRAMESET = 32,
TOOLTIP = 33,
HIDDEN = 34,
URL = 35,
TOOLBAR = 36,
WINDOW = 37,
LABEL = 38,
IMAGE = 39,
PLAINTEXT = 40,
SELECT_TREE = 41,
};
pub const ElementAreas = packed struct {
relative: enum(u4) {
ROOT = 0x1,
SELF = 0x2,
CONTAINER = 0x3,
VIEW = 0x4,
_,
} = @enumFromInt(0x0),
area: enum(u4) {
CONTENT_BOX = 0x0,
PADDING_BOX = 0x1,
BORDER_BOX = 0x2,
MARGIN_BOX = 0x3,
BACK_IMAGE_AREA = 0x4,
FORE_IMAGE_AREA = 0x5,
SCROLLABLE_AREA = 0x6,
_,
} = .CONTENT_BOX,
_pad8: u8 = 0,
as_ppx: bool = false,
_pad17: u15 = 0,
};
pub const MethodParams = extern struct {
method_id: u32,
};
pub const NodeType = enum(u32) { ELEMENT, TEXT, COMMENT };
pub const NodeInsTarget = enum(u32) { BEFORE, AFTER, APPEND, PREPEND };
pub const Point = extern struct {
x: i32,
y: i32,
};
pub const Rect = extern struct {
left: i32,
top: i32,
right: i32,
bottom: u32,
};
pub const RequestParam = extern struct {
name: CWStr,
value: CWStr,
};
pub const RequestType = enum(u32) {
GET_ASYNC,
POST_ASYNC,
GET_SYNC,
POST_SYNC,
};
pub const ResourceType = enum(u32) {
HTML = 0,
IMAGE = 1,
STYLE = 2,
CURSOR = 3,
SCRIPT = 4,
RAW = 5,
FONT,
SOUND,
FORCE_DWORD = 0xFFFFFFFF,
};
pub const SetElementHtml = enum(u32) {
SIH_REPLACE_CONTENT = 0,
SIH_INSERT_AT_START = 1,
SIH_APPEND_AFTER_LAST = 2,
SOH_REPLACE = 3,
SOH_INSERT_BEFORE = 4,
SOH_INSERT_AFTER = 5,
};
pub const Size = extern struct {
cx: i32,
cy: i32,
};
pub const Value = extern struct {
type: ValueType,
unit: extern union {
value: ValueUnit,
/// For when `Value.type == .OBJECT`
object: ValueUnitObject,
},
data: u64,
};
pub const ValueType = enum(u32) {
UNDEFINED = 0,
NULL = 1,
BOOL = 2,
i32 = 3,
FLOAT = 4,
STRING = 5,
DATE = 6,
BIG_INT = 7,
LENGTH = 8,
ARRAY = 9,
MAP = 10,
FUNCTION = 11,
BYTES = 12,
OBJECT = 13,
RESOURCE = 15,
DURATION = 17,
ANGLE = 18,
COLOR = 19,
ASSET = 21,
};
pub const ValueUnit = enum(u32) {
EM = 1,
EX = 2,
PR = 3,
SP = 4,
PX = 7,
IN = 8,
CM = 9,
MM = 10,
PT = 11,
PC = 12,
DIP = 13,
PR_WIDTH = 16,
PR_HEIGHT = 17,
PR_VIEW_WIDTH = 18,
PR_VIEW_HEIGHT = 19,
PR_VIEW_MIN = 20,
PR_VIEW_MAX = 21,
REM = 22,
PPX = 23,
CH = 24,
};
pub const ValueUnitObject = enum(u32) {
ARRAY = 0,
OBJECT = 1,
CLASS = 2,
NATIVE = 3,
FUNCTION = 4,
ERROR = 5,
BUFFER = 6,
};
pub const ValueResult = enum(i32) {
OK_TRUE = -1,
OK = 0,
BAD_PARAMETER = 1,
INCOMPATIBLE_TYPE = 2,
};
pub const OutputSubsystems = enum(u32) {
DOM = 0,
CSSS,
CSS,
TIS,
};
pub const CallbackNotification = extern struct {
code: u32,
hwnd: HWindow,
};
pub const XMsgCode = enum(u32) {
CREATE = 0,
DESTROY = 1,
SIZE = 2,
PAINT = 3,
RESOLUTION = 4,
HEARTBIT = 5,
MOUSE = 6,
KEY = 7,
FOCUS = 8,
};
pub const XMsg = extern struct {
msg: XMsgCode,
};
pub const OmAsset = extern struct {
isa: *OmAssetClass,
};
pub const OmAssetClass = extern struct {
asset_add_ref: *const fn (thing: *OmAsset) callconv(.c) c_long,
asset_release: *const fn (thing: *OmAsset) callconv(.c) c_long,
asset_get_interface: *const fn (thing: *OmAsset, name: CStr, out: *?*anyopaque) callconv(.c) c_long,
asset_get_passport: *const fn (thins: *OmAsset) callconv(.c) ?*OmPassport,
};
pub const OmPassport = opaque {};
pub const ValueStringCvtType = enum(u32) {
CVT_SIMPLE,
CVT_JSON_LITERAL,
CVT_JSON_MAP,
CVT_XJSON_LITERAL,
};
const ByteReceiver = fn (str: [*]const u8, num_bytes: u32, param: ?*anyopaque) callconv(.c) void;
const DebugOutputProc = fn (param: ?*anyopaque, subsystem: OutputSubsystems, severity: u32, text: [*]const u16, text_length: u32) callconv(.c) void;
const ElementCallback = fn (he: HElement, param: ?*anyopaque) callconv(.c) Bool;
const ElementComparator = fn (he1: HElement, he2: HElement, param: ?*anyopaque) callconv(.c) i32;
const ElementEventProc = fn (tag: ?*anyopaque, he: HElement, evtg: u32, prms: ?*anyopaque) callconv(.c) Bool;
const HostCallback = fn (pns: *CallbackNotification, callback_param: ?*anyopaque) callconv(.c) u32;
const KeyValueCallback = fn (param: ?*anyopaque, pkey: *const Value, pval: *const Value) callconv(.c) Bool;
const NativeFunctorInvoke = fn (tag: ?*anyopaque, argc: u32, argv: [*]const Value, retval: *Value) callconv(.c) void;
const NativeFunctorRelease = fn (tag: ?*anyopaque) callconv(.c) void;
const StrReceiver = fn (str: [*]const u8, num_bytes: u32, param: ?*anyopaque) callconv(.c) void;
const WindowDelegate = fn (hwnd: HWindow, msg: u32, wParam: usize, lParam: isize, pbHandled: *Bool) callconv(.c) isize;
const WStrReceiver = fn (str: [*]const u16, num_bytes: u32, param: ?*anyopaque) callconv(.c) void;
pub const API = extern struct {
version: u32,
SciterClassName: *const fn () callconv(.c) CWStr,
SciterVersion: *const fn (n: u32) callconv(.c) u32,
SciterDataReady: *const fn (hwnd: HWindow, uri: CWStr, data: [*]const u8, dataLength: u32) callconv(.c) Bool,
SciterDataReadyAsync: *const fn (hwnd: HWindow, uri: CWStr, data: [*]const u8, dataLength: u32, requestId: ?*anyopaque) callconv(.c) Bool,
SciterProc: *const fn (hwnd: HWindow, msg: u32, wParam: usize, lParam: isize) callconv(.c) isize,
SciterProcND: *const fn (hwnd: HWindow, msg: u32, wParam: usize, lParam: isize, pbHandled: *Bool) callconv(.c) isize,
SciterLoadFile: *const fn (hwnd: HWindow, filename: CWStr) callconv(.c) Bool,
SciterLoadHtml: *const fn (hwnd: HWindow, html: [*]const u8, htmlSize: u32, baseUrl: CWStr) callconv(.c) Bool,
SciterSetCallback: *const fn (hwnd: HWindow, cb: ?*HostCallback, cbParam: ?*anyopaque) callconv(.c) void,
SciterSetMasterCSS: *const fn (utf8: [*]const u8, numBytes: u32) callconv(.c) Bool,
SciterAppendMasterCSS: *const fn (utf8: [*]const u8, numBytes: u32) callconv(.c) Bool,
SciterSetCSS: *const fn (hwnd: HWindow, utf8: [*]const u8, numBytes: u32, baseUrl: CWStr, mediaType: CWStr) callconv(.c) Bool,
SciterSetMediaType: *const fn (hwnd: HWindow, mediaType: CWStr) callconv(.c) Bool,
SciterSetMediaVars: *const fn (hwnd: HWindow, mediaVars: *const Value) callconv(.c) Bool,
SciterGetMinWidth: *const fn (hwnd: HWindow) callconv(.c) u32,
SciterGetMinHeight: *const fn (hwnd: HWindow, width: u32) callconv(.c) u32,
SciterCall: *const fn (hWnd: HWindow, functionName: CStr, argc: u32, argv: [*]const Value, retval: *Value) callconv(.c) Bool,
SciterEval: *const fn (hwnd: HWindow, script: [*]const u16, scriptLength: u32, pretval: *Value) callconv(.c) Bool,
SciterUpdateWindow: *const fn (hwnd: HWindow) callconv(.c) void,
/// Win32 MSG
SciterTranslateMessage: *const fn (lpMsg: *anyopaque) callconv(.c) Bool,
SciterSetOption: *const fn (hWnd: HWindow, option: u32, value: usize) callconv(.c) Bool,
SciterGetPPI: *const fn (hwnd: HWindow, px: *u32, py: *u32) callconv(.c) void,
SciterGetViewExpando: *const fn (hwnd: HWindow, pval: *Value) callconv(.c) Bool,
/// ID2D1RenderTarget
SciterRenderD2D: *const fn (hwnd: HWindow, prt: *anyopaque) callconv(.c) Bool,
/// ID2D1Factory
SciterD2DFactory: *const fn (ppf: *anyopaque) callconv(.c) Bool,
/// IDWriteFactory
SciterDWFactory: *const fn (ppf: *anyopaque) callconv(.c) Bool,
SciterGraphicsCaps: *const fn (pcaps: *u32) callconv(.c) Bool,
SciterSetHomeURL: *const fn (hwnd: HWindow, baseUrl: CWStr) callconv(.c) Bool,
SciterCreateNSView: *anyopaque,
SciterCreateWidget: *anyopaque,
SciterCreateWindow: *const fn (creationFlags: u32, frame: *Rect, delegate: *const WindowDelegate, delegateParam: ?*anyopaque, parent: HWindow) callconv(.c) HWindow,
SciterSetupDebugOutput: *const fn (hwndOrNull: ?HWindow, param: ?*anyopaque, pfOutput: ?*DebugOutputProc) callconv(.c) void,
// --- DOM API -------------------------------------------------------------
Sciter_UseElement: *const fn (he: HElement) callconv(.c) DomResult,
Sciter_UnuseElement: *const fn (he: HElement) callconv(.c) DomResult,
SciterGetRootElement: *const fn (hwnd: HWindow, phe: *HElement) callconv(.c) DomResult,
SciterGetFocusElement: *const fn (hwnd: HWindow, phe: *HElement) callconv(.c) DomResult,
SciterFindElement: *const fn (hwnd: HWindow, pt: Point, phe: *HElement) callconv(.c) DomResult,
SciterGetChildrenCount: *const fn (he: HElement, count: *u32) callconv(.c) DomResult,
SciterGetNthChild: *const fn (he: HElement, n: u32, phe: *HElement) callconv(.c) DomResult,
SciterGetParentElement: *const fn (he: HElement, p_parent_he: *HElement) callconv(.c) DomResult,
SciterGetElementHtmlCB: *const fn (he: HElement, outer: Bool, rcv: *const ByteReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterGetElementTextCB: *const fn (he: HElement, rcv: *const WStrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterSetElementText: *const fn (he: HElement, utf16: [*]const u16, length: u32) callconv(.c) DomResult,
SciterGetAttributeCount: *const fn (he: HElement, p_count: *u32) callconv(.c) DomResult,
SciterGetNthAttributeNameCB: *const fn (he: HElement, n: u32, rcv: *const StrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterGetNthAttributeValueCB: *const fn (he: HElement, n: u32, rcv: *const WStrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterGetAttributeByNameCB: *const fn (he: HElement, name: CStr, rcv: *const WStrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterSetAttributeByName: *const fn (he: HElement, name: CStr, value: CWStr) callconv(.c) DomResult,
SciterClearAttributes: *const fn (he: HElement) callconv(.c) DomResult,
SciterGetElementIndex: *const fn (he: HElement, p_index: *u32) callconv(.c) DomResult,
SciterGetElementType: *const fn (he: HElement, p_type: *CStr) callconv(.c) DomResult,
SciterGetElementTypeCB: *const fn (he: HElement, rcv: *const StrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterGetStyleAttributeCB: *const fn (he: HElement, name: CStr, rcv: *const WStrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterSetStyleAttribute: *const fn (he: HElement, name: CStr, value: CWStr) callconv(.c) DomResult,
SciterGetElementLocation: *const fn (he: HElement, p_location: *Rect, areas: ElementAreas) callconv(.c) DomResult,
SciterScrollToView: *const fn (he: HElement, SciterScrollFlags: u32) callconv(.c) DomResult,
SciterUpdateElement: *const fn (he: HElement, andForceRender: Bool) callconv(.c) DomResult,
SciterRefreshElementArea: *const fn (he: HElement, rc: Rect) callconv(.c) DomResult,
SciterSetCapture: *const fn (he: HElement) callconv(.c) DomResult,
SciterReleaseCapture: *const fn (he: HElement) callconv(.c) DomResult,
SciterGetElementHwnd: *const fn (he: HElement, p_hwnd: *HWindow, rootWindow: Bool) callconv(.c) DomResult,
SciterCombineURL: *const fn (he: HElement, szUrlBuffer: [*]u16, UrlBufferSize: u32) callconv(.c) DomResult,
SciterSelectElements: *const fn (he: HElement, CSS_selectors: CStr, callback: *const ElementCallback, param: ?*anyopaque) callconv(.c) DomResult,
SciterSelectElementsW: *const fn (he: HElement, CSS_selectors: CWStr, callback: *const ElementCallback, param: ?*anyopaque) callconv(.c) DomResult,
SciterSelectParent: *const fn (he: HElement, selector: CStr, depth: u32, heFound: *HElement) callconv(.c) DomResult,
SciterSelectParentW: *const fn (he: HElement, selector: CWStr, depth: u32, heFound: *HElement) callconv(.c) DomResult,
SciterSetElementHtml: *const fn (he: HElement, html: [*]const u8, htmlLength: u32, where: SetElementHtml) callconv(.c) DomResult,
SciterGetElementUID: *const fn (he: HElement, puid: *u32) callconv(.c) DomResult,
SciterGetElementByUID: *const fn (hwnd: HWindow, uid: u32, phe: *HElement) callconv(.c) DomResult,
SciterShowPopup: *const fn (hePopup: HElement, heAnchor: HElement, placement: u32) callconv(.c) DomResult,
SciterShowPopupAt: *const fn (hePopup: HElement, pos: Point, placement: u32) callconv(.c) DomResult,
SciterHidePopup: *const fn (he: HElement) callconv(.c) DomResult,
SciterGetElementState: *const fn (he: HElement, pstateBits: *u32) callconv(.c) DomResult,
SciterSetElementState: *const fn (he: HElement, stateBitsToSet: u32, stateBitsToClear: u32, updateView: Bool) callconv(.c) DomResult,
SciterCreateElement: *const fn (tagname: CStr, textOrNull: ?CWStr, phe: *HElement) callconv(.c) DomResult,
SciterCloneElement: *const fn (he: HElement, phe: *HElement) callconv(.c) DomResult,
SciterInsertElement: *const fn (he: HElement, hparent: HElement, index: u32) callconv(.c) DomResult,
SciterDetachElement: *const fn (he: HElement) callconv(.c) DomResult,
SciterDeleteElement: *const fn (he: HElement) callconv(.c) DomResult,
SciterSetTimer: *const fn (he: HElement, milliseconds: u32, timer_id: usize) callconv(.c) DomResult,
SciterDetachEventHandler: *const fn (he: HElement, pep: *const ElementEventProc, tag: ?*anyopaque) callconv(.c) DomResult,
SciterAttachEventHandler: *const fn (he: HElement, pep: *const ElementEventProc, tag: ?*anyopaque) callconv(.c) DomResult,
SciterWindowAttachEventHandler: *const fn (hwndLayout: HWindow, pep: *const ElementEventProc, tag: ?*anyopaque, subscription: u32) callconv(.c) DomResult,
SciterWindowDetachEventHandler: *const fn (hwndLayout: HWindow, pep: *const ElementEventProc, tag: ?*anyopaque) callconv(.c) DomResult,
SciterSendEvent: *const fn (he: HElement, appEventCode: u32, heSource: HElement, reason: usize, handled: *Bool) callconv(.c) DomResult,
SciterPostEvent: *const fn (he: HElement, appEventCode: u32, heSource: HElement, reason: usize) callconv(.c) DomResult,
SciterCallBehaviorMethod: *const fn (he: HElement, params: *MethodParams) callconv(.c) DomResult,
SciterRequestElementData: *const fn (he: HElement, url: CWStr, dataType: u32, initiator: HElement) callconv(.c) DomResult,
SciterHttpRequest: *const fn (he: HElement, url: CWStr, dataType: ResourceType, requestType: RequestType, requestParams: [*]RequestParam, nParams: u32) callconv(.c) DomResult,
SciterGetScrollInfo: *const fn (he: HElement, scrollPos: *Point, viewRect: *Rect, contentSize: *Size) callconv(.c) DomResult,
SciterSetScrollPos: *const fn (he: HElement, scrollPos: Point, smooth: Bool) callconv(.c) DomResult,
SciterGetElementIntrinsicWidths: *const fn (he: HElement, pMinWidth: *i32, pMaxWidth: *i32) callconv(.c) DomResult,
SciterGetElementIntrinsicHeight: *const fn (he: HElement, forWidth: i32, pHeight: *i32) callconv(.c) DomResult,
SciterIsElementVisible: *const fn (he: HElement, pVisible: *Bool) callconv(.c) DomResult,
SciterIsElementEnabled: *const fn (he: HElement, pEnabled: *Bool) callconv(.c) DomResult,
SciterSortElements: *const fn (he: HElement, firstIndex: u32, lastIndex: u32, cmpFunc: *const ElementComparator, cmpFuncParam: ?*anyopaque) callconv(.c) DomResult,
SciterSwapElements: *const fn (he1: HElement, he2: HElement) callconv(.c) DomResult,
SciterTraverseUIEvent: *const fn (evt: u32, eventCtlStruct: ?*anyopaque, bOutProcessed: *Bool) callconv(.c) DomResult,
SciterCallScriptingMethod: *const fn (he: HElement, name: CStr, argv: [*]const Value, argc: u32, retval: *Value) callconv(.c) DomResult,
SciterCallScriptingFunction: *const fn (he: HElement, name: CStr, argv: [*]const Value, argc: u32, retval: *Value) callconv(.c) DomResult,
SciterEvalElementScript: *const fn (he: HElement, script: [*]const u16, scriptLength: u32, retval: *Value) callconv(.c) DomResult,
SciterAttachHwndToElement: *const fn (he: HElement, hwnd: HWindow) callconv(.c) DomResult,
SciterControlGetType: *const fn (he: HElement, pType: *CtlType) callconv(.c) DomResult,
SciterGetValue: *const fn (he: HElement, pval: *Value) callconv(.c) DomResult,
SciterSetValue: *const fn (he: HElement, pval: *const Value) callconv(.c) DomResult,
SciterGetExpando: *const fn (he: HElement, pval: *Value, forceCreation: Bool) callconv(.c) DomResult,
SciterGetObject: *const fn (he: HElement, pval: *void, forceCreation: Bool) callconv(.c) DomResult,
SciterGetElementNamespace: *const fn (he: HElement, pval: *void) callconv(.c) DomResult,
SciterGetHighlightedElement: *const fn (hwnd: HWindow, phe: *HElement) callconv(.c) DomResult,
SciterSetHighlightedElement: *const fn (hwnd: HWindow, he: HElement) callconv(.c) DomResult,
// --- DOM NODE API --------------------------------------------------------
SciterNodeAddRef: *const fn (hn: HNode) callconv(.c) DomResult,
SciterNodeRelease: *const fn (hn: HNode) callconv(.c) DomResult,
SciterNodeCastFromElement: *const fn (he: HElement, phn: *HNode) callconv(.c) DomResult,
SciterNodeCastToElement: *const fn (hn: HNode, he: *HElement) callconv(.c) DomResult,
SciterNodeFirstChild: *const fn (hn: HNode, phn: *HNode) callconv(.c) DomResult,
SciterNodeLastChild: *const fn (hn: HNode, phn: *HNode) callconv(.c) DomResult,
SciterNodeNextSibling: *const fn (hn: HNode, phn: *HNode) callconv(.c) DomResult,
SciterNodePrevSibling: *const fn (hn: HNode, phn: *HNode) callconv(.c) DomResult,
SciterNodeParent: *const fn (hnode: HNode, pheParent: *HElement) callconv(.c) DomResult,
SciterNodeNthChild: *const fn (hnode: HNode, n: u32, phn: *HNode) callconv(.c) DomResult,
SciterNodeChildrenCount: *const fn (hnode: HNode, pn: *u32) callconv(.c) DomResult,
SciterNodeType: *const fn (hnode: HNode, pNodeType: *NodeType) callconv(.c) DomResult,
SciterNodeGetText: *const fn (hnode: HNode, rcv: *const WStrReceiver, rcv_param: ?*anyopaque) callconv(.c) DomResult,
SciterNodeSetText: *const fn (hnode: HNode, text: [*]const u16, textLength: u32) callconv(.c) DomResult,
SciterNodeInsert: *const fn (hnode: HNode, where: NodeInsTarget, what: HNode) callconv(.c) DomResult,
SciterNodeRemove: *const fn (hnode: HNode, finalize: Bool) callconv(.c) DomResult,
SciterCreateTextNode: *const fn (text: [*]const u16, textLength: u32, phnode: *HNode) callconv(.c) DomResult,
SciterCreateCommentNode: *const fn (text: [*]const u16, textLength: u32, phnode: *HNode) callconv(.c) DomResult,
// --- Value API -----------------------------------------------------------
ValueInit: *const fn (pval: *Value) callconv(.c) u32,
ValueClear: *const fn (pval: *Value) callconv(.c) u32,
ValueCompare: *const fn (pval1: *const Value, pval2: *const Value) callconv(.c) u32,
ValueCopy: *const fn (pdst: *Value, psrc: *const Value) callconv(.c) u32,
ValueIsolate: *const fn (pdst: *Value) callconv(.c) u32,
ValueType: *const fn (pval: *const Value, pType: *u32, pUnits: *u32) callconv(.c) u32,
ValueStringData: *const fn (pval: *const Value, pChars: *[*]const u16, pNumChars: *u32) callconv(.c) u32,
ValueStringDataSet: *const fn (pval: *Value, chars: [*]const u16, numChars: u32, units: u32) callconv(.c) u32,
ValueIntData: *const fn (pval: *const Value, pData: *i32) callconv(.c) u32,
ValueIntDataSet: *const fn (pval: *Value, data: i32, type: u32, units: u32) callconv(.c) u32,
ValueInt64Data: *const fn (pval: *const Value, pData: *i64) callconv(.c) u32,
ValueInt64DataSet: *const fn (pval: *Value, data: i64, type: u32, units: u32) callconv(.c) u32,
ValueFloatData: *const fn (pval: *const Value, pData: *f64) callconv(.c) u32,
ValueFloatDataSet: *const fn (pval: *Value, data: f64, type: u32, units: u32) callconv(.c) u32,
ValueBinaryData: *const fn (pval: *const Value, pBytes: *[*]const u8, pnBytes: *u32) callconv(.c) u32,
ValueBinaryDataSet: *const fn (pval: *Value, pBytes: [*]const u8, nBytes: u32, type: u32, units: u32) callconv(.c) u32,
ValueElementsCount: *const fn (pval: *const Value, pn: *i32) callconv(.c) u32,
ValueNthElementValue: *const fn (pval: *const Value, n: i32, pretval: *Value) callconv(.c) u32,
ValueNthElementValueSet: *const fn (pval: *Value, n: i32, pval_to_set: *const Value) callconv(.c) u32,
ValueNthElementKey: *const fn (pval: *const Value, n: i32, pretval: *Value) callconv(.c) u32,
ValueEnumElements: *const fn (pval: *const Value, penum: *const KeyValueCallback, param: ?*anyopaque) callconv(.c) u32,
ValueSetValueToKey: *const fn (pval: *Value, pkey: *const Value, pval_to_set: *const Value) callconv(.c) u32,
ValueGetValueOfKey: *const fn (pval: *const Value, pkey: *const Value, pretval: *Value) callconv(.c) u32,
ValueToString: *const fn (pval: *Value, how: ValueStringCvtType) callconv(.c) u32,
ValueFromString: *const fn (pval: *Value, str: [*]const u16, strLength: u32, how: ValueStringCvtType) callconv(.c) u32,
ValueInvoke: *const fn (pval: *const Value, pthis: *Value, argc: u32, argv: *const Value, pretval: *Value, url: CWStr) callconv(.c) u32,
ValueNativeFunctorSet: *const fn (pval: *Value, pinvoke: *const NativeFunctorInvoke, prelease: *const NativeFunctorRelease, tag: *void) callconv(.c) u32,
ValueIsNativeFunctor: *const fn (pval: *const Value) callconv(.c) Bool,
// Used to be script VM API
reserved1: *anyopaque,
reserved2: *anyopaque,
reserved3: *anyopaque,
reserved4: *anyopaque,
SciterOpenArchive: *const fn (archiveData: [*]const u8, archiveDataLength: u32) callconv(.c) HSArchive,
SciterGetArchiveItem: *const fn (harc: HSArchive, path: CWStr, pdata: *[*]const u8, pdataLength: *u32) callconv(.c) Bool,
SciterCloseArchive: *const fn (harc: HSArchive) callconv(.c) Bool,
SciterFireEvent: *const fn (evt: *const BehaviorEventParams, post: Bool, handled: *Bool) callconv(.c) DomResult,
SciterGetCallbackParam: *const fn (hwnd: HWindow) callconv(.c) ?*anyopaque,
SciterPostCallback: *const fn (hwnd: HWindow, wparam: isize, lparam: usize, timeoutms: u32) callconv(.c) isize,
GetSciterGraphicsAPI: *const fn () callconv(.c) *GraphicsAPI,
GetSciterRequestAPI: *const fn () callconv(.c) *RequestAPI,
/// IDXGISwapChain
SciterCreateOnDirectXWindow: *const fn (hwnd: HWindow, pSwapChain: *anyopaque) callconv(.c) Bool,
SciterRenderOnDirectXWindow: *const fn (hwnd: HWindow, elementToRenderOrNull: HElement, frontLayer: Bool) callconv(.c) Bool,
/// IDXGISurface
SciterRenderOnDirectXTexture: *const fn (hwnd: HWindow, elementToRenderOrNull: HElement, surface: *anyopaque) callconv(.c) Bool,
SciterProcX: *const fn (hwnd: HWindow, pMsg: *XMsg) callconv(.c) Bool,
SciterAtomValue: *const fn (name: CStr) callconv(.c) u64,
SciterAtomNameCB: *const fn (atomv: u64, rcv: *const StrReceiver, rcv_param: ?*anyopaque) callconv(.c) Bool,
SciterSetGlobalAsset: *const fn (pass: *OmAsset) callconv(.c) Bool,
SciterGetElementAsset: *const fn (el: HElement, nameAtom: u64, ppass: *?*OmAsset) callconv(.c) DomResult,
SciterSetVariable: *const fn (hwndOrNull: HWindow, name: CStr, pvalToSet: *const Value) callconv(.c) u32,
SciterGetVariable: *const fn (hwndOrNull: HWindow, name: CStr, pvalToGet: *Value) callconv(.c) u32,
SciterElementUnwrap: *const fn (pval: *const Value, ppElement: *HElement) callconv(.c) u32,
SciterElementWrap: *const fn (pval: *Value, pElement: HElement) callconv(.c) u32,
SciterNodeUnwrap: *const fn (pval: *const Value, ppNode: *HNode) callconv(.c) u32,
SciterNodeWrap: *const fn (pval: *Value, pNode: HNode) callconv(.c) u32,
SciterReleaseGlobalAsset: *const fn (pass: *OmAsset) callconv(.c) Bool,
SciterExec: *const fn (appCmd: u32, p1: usize, p2: usize) callconv(.c) isize,
SciterWindowExec: *const fn (hwnd: HWindow, windowCmd: u32, p1: usize, p2: usize) callconv(.c) isize,
SciterEGLGetProcAddress: *const fn (procName: CStr) callconv(.c) ?*anyopaque,
SciterEGLSendEvent: *const fn (he: HElement, eventCode: u32, reason: usize) callconv(.c) DomResult,
SciterRequestAnimationFrameEvent: *const fn (he: HElement, eventCode: u32, reason: usize) callconv(.c) DomResult,
};
// TODO These APIs are actually well-known structs with function pointers in
// them, which may provide useful functionality
pub const GraphicsAPI = opaque {};
pub const RequestAPI = opaque {};

BIN
packages/sciter/vendor/sciter.dll (Stored with Git LFS) vendored

Binary file not shown.

View File

@@ -1,7 +1,19 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
_ = b.addModule("vecmath", .{
const target = b.standardTargetOptions(.{});
const mod = b.addModule("vecmath", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
});
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}

View File

@@ -1,7 +1,7 @@
.{
.name = .vecmath,
.version = "0.0.0",
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.paths = .{
"src",
"build.zig",

View File

@@ -63,33 +63,41 @@ pub const Color = extern struct {
@compileError("Invalid color literal: " ++ literal);
}
test l {
const i = Color.l("#012");
try std.testing.expectEqual(0x00, i.r);
try std.testing.expectEqual(0x11, i.g);
try std.testing.expectEqual(0x22, i.b);
try std.testing.expectEqual(0xFF, i.a);
const j = Color.l("#3456");
try std.testing.expectEqual(0x33, j.r);
try std.testing.expectEqual(0x44, j.g);
try std.testing.expectEqual(0x55, j.b);
try std.testing.expectEqual(0x66, j.a);
const k = Color.l("#F08040");
try std.testing.expectEqual(0xF0, k.r);
try std.testing.expectEqual(0x80, k.g);
try std.testing.expectEqual(0x40, k.b);
try std.testing.expectEqual(0xFF, k.a);
const m = Color.l("#20304050");
try std.testing.expectEqual(0x20, m.r);
try std.testing.expectEqual(0x30, m.g);
try std.testing.expectEqual(0x40, m.b);
try std.testing.expectEqual(0x50, m.a);
}
pub inline fn asArray(self: Color) Array {
return @bitCast(self);
}
pub fn format(self: Color, w: *std.Io.Writer) !void {
try w.print("#{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ self.r, self.g, self.b, self.a });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};
test "l" {
const i: Color = .l("#012");
try std.testing.expectEqual(0x00, i.r);
try std.testing.expectEqual(0x11, i.g);
try std.testing.expectEqual(0x22, i.b);
try std.testing.expectEqual(0xFF, i.a);
const j: Color = .l("#3456");
try std.testing.expectEqual(0x33, j.r);
try std.testing.expectEqual(0x44, j.g);
try std.testing.expectEqual(0x55, j.b);
try std.testing.expectEqual(0x66, j.a);
const k: Color = .l("#F08040");
try std.testing.expectEqual(0xF0, k.r);
try std.testing.expectEqual(0x80, k.g);
try std.testing.expectEqual(0x40, k.b);
try std.testing.expectEqual(0xFF, k.a);
const l: Color = .l("#20304050");
try std.testing.expectEqual(0x20, l.r);
try std.testing.expectEqual(0x30, l.g);
try std.testing.expectEqual(0x40, l.b);
try std.testing.expectEqual(0x50, l.a);
}

View File

@@ -0,0 +1,34 @@
const std = @import("std");
const vm = @import("../root.zig");
pub const ColorHdr = extern struct {
r: f16,
g: f16,
b: f16,
a: f16,
pub const Array = [4]f16;
pub const zero = init(0, 0, 0, 0);
pub const one = init(1, 1, 1, 1);
pub inline fn init(r: f16, g: f16, b: f16, a: f16) ColorHdr {
return .{ .r = r, .g = g, .b = b, .a = a };
}
pub inline fn initArray(array: Array) ColorHdr {
return @bitCast(array);
}
pub inline fn asArray(self: ColorHdr) Array {
return @bitCast(self);
}
pub fn format(self: ColorHdr, w: *std.Io.Writer) !void {
try w.print("ColorHdr[{d}, {d}, {d}, {d}]", .{ self.r, self.g, self.b, self.a });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -252,4 +252,8 @@ pub const Matrix3x2 = extern struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -406,4 +406,8 @@ pub const Matrix3x2x8 = struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -75,9 +75,9 @@ pub const Matrix4x4 = extern struct {
return .{
// zig fmt: off
.ix = 1 - 2 * (yy + zz), .iy = 2 * (xy - zw), .iz = 2 * (xz + yw), .iw = 0,
.jx = 2 * (xy + zw), .jy = 1 - 2 * (xx + zz), .jz = 2 * (yz - xw), .jw = 0,
.kx = 2 * (xz - yw), .ky = 2 * (yz + xw), .kz = 1 - 2 * (xx + yy), .kw = 0,
.ix = 1 - 2 * (yy + zz), .iy = 2 * (xy + zw), .iz = 2 * (xz - yw), .iw = 0,
.jx = 2 * (xy - zw), .jy = 1 - 2 * (xx + zz), .jz = 2 * (yz + xw), .jw = 0,
.kx = 2 * (xz + yw), .ky = 2 * (yz - xw), .kz = 1 - 2 * (xx + yy), .kw = 0,
.tx = 0, .ty = 0, .tz = 0, .tw = 1,
// zig fmt: on
};
@@ -121,9 +121,9 @@ pub const Matrix4x4 = extern struct {
return .{
// zig fmt: off
.ix = 1 - 2 * (yy + zz), .iy = 2 * (xy - zw), .iz = 2 * (xz + yw), .iw = 0,
.jx = 2 * (xy + zw), .jy = 1 - 2 * (xx + zz), .jz = 2 * (yz - xw), .jw = 0,
.kx = 2 * (xz - yw), .ky = 2 * (yz + xw), .kz = 1 - 2 * (xx + yy), .kw = 0,
.ix = 1 - 2 * (yy + zz), .iy = 2 * (xy + zw), .iz = 2 * (xz - yw), .iw = 0,
.jx = 2 * (xy - zw), .jy = 1 - 2 * (xx + zz), .jz = 2 * (yz + xw), .jw = 0,
.kx = 2 * (xz + yw), .ky = 2 * (yz - xw), .kz = 1 - 2 * (xx + yy), .kw = 0,
.tx = t.x, .ty = t.y, .tz = t.z, .tw = 1,
// zig fmt: on
};
@@ -153,9 +153,9 @@ pub const Matrix4x4 = extern struct {
return .{
// zig fmt: off
.ix = s.x * (1 - 2 * (yy + zz)), .iy = s.x * 2 * (xy - zw), .iz = s.x * 2 * (xz + yw), .iw = 0,
.jx = s.y * 2 * (xy + zw), .jy = s.y * (1 - 2 * (xx + zz)), .jz = s.y * 2 * (yz - xw), .jw = 0,
.kx = s.z * 2 * (xz - yw), .ky = s.z * 2 * (yz + xw), .kz = s.z * (1 - 2 * (xx + yy)), .kw = 0,
.ix = s.x * (1 - 2 * (yy + zz)), .iy = s.x * 2 * (xy + zw), .iz = s.x * 2 * (xz - yw), .iw = 0,
.jx = s.y * 2 * (xy - zw), .jy = s.y * (1 - 2 * (xx + zz)), .jz = s.y * 2 * (yz + xw), .jw = 0,
.kx = s.z * 2 * (xz + yw), .ky = s.z * 2 * (yz - xw), .kz = s.z * (1 - 2 * (xx + yy)), .kw = 0,
.tx = t.x, .ty = t.y, .tz = t.z, .tw = 1,
// zig fmt: on
};
@@ -507,4 +507,8 @@ pub const Matrix4x4 = extern struct {
// zig fmt: on
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -113,9 +113,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = vm.ps(1) - vm.ps(2) * (yy + zz), .iy = vm.ps(2) * (xy - zw), .iz = vm.ps(2) * (xz + yw), .iw = vm.ps(0),
.jx = vm.ps(2) * (xy + zw), .jy = vm.ps(1) - vm.ps(2) * (xx + zz), .jz = vm.ps(2) * (yz - xw), .jw = vm.ps(0),
.kx = vm.ps(2) * (xz - yw), .ky = vm.ps(2) * (yz + xw), .kz = vm.ps(1) - vm.ps(2) * (xx + yy), .kw = vm.ps(0),
.ix = vm.ps(1) - vm.ps(2) * (yy + zz), .iy = vm.ps(2) * (xy + zw), .iz = vm.ps(2) * (xz - yw), .iw = vm.ps(0),
.jx = vm.ps(2) * (xy - zw), .jy = vm.ps(1) - vm.ps(2) * (xx + zz), .jz = vm.ps(2) * (yz + xw), .jw = vm.ps(0),
.kx = vm.ps(2) * (xz + yw), .ky = vm.ps(2) * (yz - xw), .kz = vm.ps(1) - vm.ps(2) * (xx + yy), .kw = vm.ps(0),
.tx = vm.ps(0), .ty = vm.ps(0), .tz = vm.ps(0), .tw = vm.ps(1),
// zig fmt: on
};
@@ -134,9 +134,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = vm.ps(1 - 2 * (yy + zz)), .iy = vm.ps(2 * (xy - zw)), .iz = vm.ps(2 * (xz + yw)), .iw = vm.ps(0),
.jx = vm.ps(2 * (xy + zw)), .jy = vm.ps(1 - 2 * (xx + zz)), .jz = vm.ps(2 * (yz - xw)), .jw = vm.ps(0),
.kx = vm.ps(2 * (xz - yw)), .ky = vm.ps(2 * (yz + xw)), .kz = vm.ps(1 - 2 * (xx + yy)), .kw = vm.ps(0),
.ix = vm.ps(1 - 2 * (yy + zz)), .iy = vm.ps(2 * (xy + zw)), .iz = vm.ps(2 * (xz - yw)), .iw = vm.ps(0),
.jx = vm.ps(2 * (xy - zw)), .jy = vm.ps(1 - 2 * (xx + zz)), .jz = vm.ps(2 * (yz + xw)), .jw = vm.ps(0),
.kx = vm.ps(2 * (xz + yw)), .ky = vm.ps(2 * (yz - xw)), .kz = vm.ps(1 - 2 * (xx + yy)), .kw = vm.ps(0),
.tx = vm.ps(0), .ty = vm.ps(0), .tz = vm.ps(0), .tw = vm.ps(1),
// zig fmt: on
};
@@ -177,9 +177,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = vm.ps(1) - vm.ps(2) * (yy + zz), .iy = vm.ps(2) * (xy - zw), .iz = vm.ps(2) * (xz + yw), .iw = vm.ps(0),
.jx = vm.ps(2) * (xy + zw), .jy = vm.ps(1) - vm.ps(2) * (xx + zz), .jz = vm.ps(2) * (yz - xw), .jw = vm.ps(0),
.kx = vm.ps(2) * (xz - yw), .ky = vm.ps(2) * (yz + xw), .kz = vm.ps(1) - vm.ps(2) * (xx + yy), .kw = vm.ps(0),
.ix = vm.ps(1) - vm.ps(2) * (yy + zz), .iy = vm.ps(2) * (xy + zw), .iz = vm.ps(2) * (xz - yw), .iw = vm.ps(0),
.jx = vm.ps(2) * (xy - zw), .jy = vm.ps(1) - vm.ps(2) * (xx + zz), .jz = vm.ps(2) * (yz + xw), .jw = vm.ps(0),
.kx = vm.ps(2) * (xz + yw), .ky = vm.ps(2) * (yz - xw), .kz = vm.ps(1) - vm.ps(2) * (xx + yy), .kw = vm.ps(0),
.tx = t.x, .ty = t.y, .tz = t.z, .tw = vm.ps(1),
// zig fmt: on
};
@@ -198,9 +198,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = vm.ps(1 - 2 * (yy + zz)), .iy = vm.ps(2 * (xy - zw)), .iz = vm.ps(2 * (xz + yw)), .iw = vm.ps(0),
.jx = vm.ps(2 * (xy + zw)), .jy = vm.ps(1 - 2 * (xx + zz)), .jz = vm.ps(2 * (yz - xw)), .jw = vm.ps(0),
.kx = vm.ps(2 * (xz - yw)), .ky = vm.ps(2 * (yz + xw)), .kz = vm.ps(1 - 2 * (xx + yy)), .kw = vm.ps(0),
.ix = vm.ps(1 - 2 * (yy + zz)), .iy = vm.ps(2 * (xy + zw)), .iz = vm.ps(2 * (xz - yw)), .iw = vm.ps(0),
.jx = vm.ps(2 * (xy - zw)), .jy = vm.ps(1 - 2 * (xx + zz)), .jz = vm.ps(2 * (yz + xw)), .jw = vm.ps(0),
.kx = vm.ps(2 * (xz + yw)), .ky = vm.ps(2 * (yz - xw)), .kz = vm.ps(1 - 2 * (xx + yy)), .kw = vm.ps(0),
.tx = vm.ps(t.x), .ty = vm.ps(t.y), .tz = vm.ps(t.z), .tw = vm.ps(1),
// zig fmt: on
};
@@ -241,9 +241,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = s.x * (vm.ps(1) - vm.ps(2) * (yy + zz)), .iy = s.x * vm.ps(2) * (xy - zw), .iz = s.x * vm.ps(2) * (xz + yw), .iw = vm.ps(0),
.jx = s.y * vm.ps(2) * (xy + zw), .jy = s.y * (vm.ps(1) - vm.ps(2) * (xx + zz)), .jz = s.y * vm.ps(2) * (yz - xw), .jw = vm.ps(0),
.kx = s.z * vm.ps(2) * (xz - yw), .ky = s.z * vm.ps(2) * (yz + xw), .kz = s.z * (vm.ps(1) - vm.ps(2) * (xx + yy)), .kw = vm.ps(0),
.ix = s.x * (vm.ps(1) - vm.ps(2) * (yy + zz)), .iy = s.x * vm.ps(2) * (xy + zw), .iz = s.x * vm.ps(2) * (xz - yw), .iw = vm.ps(0),
.jx = s.y * vm.ps(2) * (xy - zw), .jy = s.y * (vm.ps(1) - vm.ps(2) * (xx + zz)), .jz = s.y * vm.ps(2) * (yz + xw), .jw = vm.ps(0),
.kx = s.z * vm.ps(2) * (xz + yw), .ky = s.z * vm.ps(2) * (yz - xw), .kz = s.z * (vm.ps(1) - vm.ps(2) * (xx + yy)), .kw = vm.ps(0),
.tx = t.x, .ty = t.y, .tz = t.z, .tw = vm.ps(1),
// zig fmt: on
};
@@ -262,9 +262,9 @@ pub const Matrix4x4x8 = extern struct {
return .{
// zig fmt: off
.ix = vm.ps(s.x * (1 - 2 * (yy + zz))), .iy = vm.ps(s.x * 2 * (xy - zw)), .iz = vm.ps(s.x * 2 * (xz + yw)), .iw = vm.ps(0),
.jx = vm.ps(s.y * 2 * (xy + zw)), .jy = vm.ps(s.y * (1 - 2 * (xx + zz))), .jz = vm.ps(s.y * 2 * (yz - xw)), .jw = vm.ps(0),
.kx = vm.ps(s.z * 2 * (xz - yw)), .ky = vm.ps(s.z * 2 * (yz + xw)), .kz = vm.ps(s.z * (1 - 2 * (xx + yy))), .kw = vm.ps(0),
.ix = vm.ps(s.x * (1 - 2 * (yy + zz))), .iy = vm.ps(s.x * 2 * (xy + zw)), .iz = vm.ps(s.x * 2 * (xz - yw)), .iw = vm.ps(0),
.jx = vm.ps(s.y * 2 * (xy - zw)), .jy = vm.ps(s.y * (1 - 2 * (xx + zz))), .jz = vm.ps(s.y * 2 * (yz + xw)), .jw = vm.ps(0),
.kx = vm.ps(s.z * 2 * (xz + yw)), .ky = vm.ps(s.z * 2 * (yz - xw)), .kz = vm.ps(s.z * (1 - 2 * (xx + yy))), .kw = vm.ps(0),
.tx = vm.ps(t.x), .ty = vm.ps(t.y), .tz = vm.ps(t.z), .tw = vm.ps(1),
// zig fmt: on
};
@@ -763,4 +763,8 @@ pub const Matrix4x4x8 = extern struct {
// zig fmt: on
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -3,6 +3,7 @@ const std = @import("std");
// --- COLORS ------------------------------------------------------------------
pub const Color = @import("colors/Color.zig").Color;
pub const ColorHdr = @import("colors/ColorHdr.zig").ColorHdr;
// --- MATRICES ----------------------------------------------------------------
@@ -78,10 +79,36 @@ pub const cossin_x8 = trig.cossin_x8;
// -----------------------------------------------------------------------------
pub inline fn lerp(comptime T: type, a: T, b: T, t: T) T {
return @mulAdd(T, t, b, @mulAdd(T, -t, a, a));
pub inline fn lerp(a: f32, b: f32, t: f32) f32 {
return @mulAdd(f32, t, b, @mulAdd(f32, -t, a, a));
}
pub inline fn lerpInt(a: i32, b: i32, t: f32) i32 {
const ab = b - a;
const ab_float: f32 = @floatFromInt(ab);
const d: i32 = @intFromFloat(@round(t * ab_float));
return a + d;
}
pub inline fn lerpInt64(a: i64, b: i64, t: f32) i64 {
const ab = b - a;
const ab_float: f32 = @floatFromInt(ab);
const d: i64 = @intFromFloat(@round(t * ab_float));
return a + d;
}
pub inline fn unlerp(a: f32, b: f32, x: f32) f32 {
return (x - a) / (b - a);
}
pub inline fn unlerpInt(a: i32, b: i32, x: i32) f32 {
return @as(f32, @floatFromInt(x - a)) / @as(f32, @floatFromInt(b - a));
}
pub inline fn unlerpInt64(a: i64, b: i64, x: i64) f32 {
return @as(f32, @floatFromInt(x - a)) / @as(f32, @floatFromInt(b - a));
}
test "refAllDecls" {
std.testing.refAllDeclsRecursive(@This());
std.testing.refAllDecls(@This());
}

View File

@@ -122,4 +122,12 @@ pub const Complex = extern struct {
.im = @mulAdd(f32, t, b.im, @mulAdd(f32, -t, a.im, a.im)),
};
}
pub fn format(self: Complex, w: *std.Io.Writer) !void {
try w.print("Complex[{d:.3}, {d:.3}]", .{ self.re, self.im });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -193,4 +193,8 @@ pub const Complex_x8 = struct {
.im = @mulAdd(vm.f32x8, vm.ps(t), b.im, @mulAdd(vm.f32x8, -vm.ps(t), a.im, a.im)),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -178,4 +178,12 @@ pub const Quaternion = extern struct {
.w = @mulAdd(f32, t, b.w, @mulAdd(f32, -t, a.w, a.w)),
};
}
pub fn format(self: Quaternion, w: *std.Io.Writer) !void {
try w.print("Quaternion[{d:.3}, {d:.3}, {d:.3}, {d:.3}]", .{ self.x, self.y, self.z, self.w });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -278,4 +278,8 @@ pub const Quaternion_x8 = struct {
.w = @mulAdd(vm.f32x8, vm.ps(t), b.w, @mulAdd(vm.f32x8, -vm.ps(t), a.w, a.w)),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -31,3 +31,7 @@ pub inline fn epi64(value: i64) i64x4 {
pub inline fn epu64(value: u64) u64x4 {
return @splat(value);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -235,3 +235,7 @@ test cossin_x8 {
cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }),
);
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -34,6 +34,10 @@ pub const Vector2 = extern struct {
return @bitCast(self);
}
pub inline fn asInt(self: Vector2) vm.Vector2Int {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y) };
}
pub inline fn withZ(self: Vector2, z: f32) vm.Vector3 {
return .{ .x = self.x, .y = self.y, .z = z };
}
@@ -162,4 +166,12 @@ pub const Vector2 = extern struct {
.y = vm.ps(self.x) * m.iy + vm.ps(self.y) * m.jy,
};
}
pub fn format(self: Vector2, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}]", .{ self.x, self.y });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -34,6 +34,10 @@ pub const Vector2Int = extern struct {
return @bitCast(self);
}
pub inline fn asFloat(self: Vector2Int) vm.Vector2 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y) };
}
pub inline fn withZ(self: Vector2Int, z: i32) vm.Vector3Int {
return .{ .x = self.x, .y = self.y, .z = z };
}
@@ -105,4 +109,15 @@ pub const Vector2Int = extern struct {
pub inline fn cross(self: Vector2Int, other: Vector2Int) i32 {
return self.x * other.y - self.y * other.x;
}
pub fn format(self: Vector2Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)),
});
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -44,6 +44,10 @@ pub const Vector2Int_x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asFloat(self: Vector2Int_x8) vm.Vector2x8 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y) };
}
pub inline fn withZ(self: Vector2Int_x8, z: vm.i32x8) vm.Vector3Int_x8 {
return .{ .x = self.x, .y = self.y, .z = z };
}
@@ -167,4 +171,8 @@ pub const Vector2Int_x8 = struct {
pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 {
return self.x * other.y - self.y * other.x;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -44,6 +44,10 @@ pub const Vector2x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asInt(self: Vector2x8) vm.Vector2Int_x8 {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y) };
}
pub inline fn withZ(self: Vector2x8, z: vm.f32x8) vm.Vector3x8 {
return .{ .x = self.x, .y = self.y, .z = z };
}
@@ -227,4 +231,8 @@ pub const Vector2x8 = struct {
.y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -37,6 +37,10 @@ pub const Vector3 = extern struct {
return @bitCast(self);
}
pub inline fn asInt(self: Vector3) vm.Vector3Int {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y), .z = @intFromFloat(self.z) };
}
pub inline fn dropZ(self: Vector3) vm.Vector2 {
return .{ .x = self.x, .y = self.y };
}
@@ -145,7 +149,7 @@ pub const Vector3 = extern struct {
pub inline fn rotate_x8(self: Vector3, quaternion: vm.Quaternion_x8) vm.Vector3x8 {
const w = quaternion.getScalar();
const xyz = quaternion.getVector();
const self_x8: vm.Vector3x8 = .splat(self);
const self_x8 = vm.Vector3x8.splat(self);
return .add(
self_x8,
@@ -187,4 +191,12 @@ pub const Vector3 = extern struct {
.z = vm.ps(self.x) * m.iz + vm.ps(self.y) * m.jz + vm.ps(self.z) * m.kz,
};
}
pub fn format(self: Vector3, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}, {d}]", .{ self.x, self.y, self.z });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -37,6 +37,10 @@ pub const Vector3Int = extern struct {
return @bitCast(self);
}
pub inline fn asFloat(self: Vector3Int) vm.Vector3 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y), .z = @floatFromInt(self.z) };
}
pub inline fn dropZ(self: Vector3Int) vm.Vector2Int {
return .{ .x = self.x, .y = self.y };
}
@@ -112,4 +116,16 @@ pub const Vector3Int = extern struct {
.z = self.x * other.y - self.y * other.x,
};
}
pub fn format(self: Vector3Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)),
@as(u32, @bitCast(self.z)),
});
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -48,6 +48,10 @@ pub const Vector3Int_x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asFloat(self: Vector3Int_x8) vm.Vector3x8 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y), .z = @floatFromInt(self.z) };
}
pub inline fn dropZ(self: Vector3Int_x8) vm.Vector2Int_x8 {
return .{ .x = self.x, .y = self.y };
}
@@ -176,4 +180,8 @@ pub const Vector3Int_x8 = struct {
.z = self.x * other.y - self.y * other.x,
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -48,6 +48,10 @@ pub const Vector3x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asInt(self: Vector3x8) vm.Vector3Int_x8 {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y), .z = @intFromFloat(self.z) };
}
pub inline fn dropZ(self: Vector3x8) vm.Vector2x8 {
return .{ .x = self.x, .y = self.y };
}
@@ -254,4 +258,8 @@ pub const Vector3x8 = struct {
.z = v.x * vm.ps(self.iz) + v.y * vm.ps(self.jz) + v.z * vm.ps(self.kz),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -36,6 +36,10 @@ pub const Vector4 = extern struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asInt(self: Vector4) vm.Vector4Int {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y), .z = @intFromFloat(self.z), .w = @intFromFloat(self.w) };
}
pub inline fn asArray(self: Vector4) Array {
return @bitCast(self);
}
@@ -142,4 +146,12 @@ pub const Vector4 = extern struct {
.w = vm.ps(self.x) * m.iw + vm.ps(self.y) * m.jw + vm.ps(self.z) * m.kw + vm.ps(self.w) * m.tw,
};
}
pub fn format(self: Vector4, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}, {d}, {d}]", .{ self.x, self.y, self.z, self.w });
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -40,6 +40,10 @@ pub const Vector4Int = extern struct {
return @bitCast(self);
}
pub inline fn asFloat(self: Vector4Int) vm.Vector4 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y), .z = @floatFromInt(self.z), .w = @floatFromInt(self.w) };
}
pub inline fn dropW(self: Vector4Int) vm.Vector3Int {
return .{ .x = self.x, .y = self.y, .z = self.z };
}
@@ -107,4 +111,17 @@ pub const Vector4Int = extern struct {
pub inline fn dot(self: Vector4Int, other: Vector4Int) i32 {
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
}
pub fn format(self: Vector4Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)),
@as(u32, @bitCast(self.z)),
@as(u32, @bitCast(self.w)),
});
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -52,6 +52,10 @@ pub const Vector4Int_x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asFloat(self: Vector4Int_x8) vm.Vector4x8 {
return .{ .x = @floatFromInt(self.x), .y = @floatFromInt(self.y), .z = @floatFromInt(self.z), .w = @floatFromInt(self.w) };
}
pub inline fn dropW(self: Vector4Int_x8) vm.Vector3Int_x8 {
return .{ .x = self.x, .y = self.y, .z = self.z };
}
@@ -173,4 +177,8 @@ pub const Vector4Int_x8 = struct {
pub inline fn dot(self: Vector4Int_x8, other: Vector4Int_x8) vm.i32x8 {
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

View File

@@ -52,6 +52,10 @@ pub const Vector4x8 = struct {
// --- CONVERSION ----------------------------------------------------------
pub inline fn asInt(self: Vector4x8) vm.Vector4Int_x8 {
return .{ .x = @intFromFloat(self.x), .y = @intFromFloat(self.y), .z = @intFromFloat(self.z), .w = @intFromFloat(self.w) };
}
pub inline fn dropW(self: Vector4x8) vm.Vector3x8 {
return .{ .x = self.x, .y = self.y, .z = self.z };
}
@@ -213,4 +217,8 @@ pub const Vector4x8 = struct {
.w = self.x * vm.ps(m.iw) + self.y * vm.ps(m.jw) + self.z * vm.ps(m.kw) + self.w * vm.ps(m.tw),
};
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
};

52
packages/web/build.zig Normal file
View File

@@ -0,0 +1,52 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const module = b.addModule("web", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
module.linkSystemLibrary("ssl", .{});
module.linkSystemLibrary("crypto", .{});
const tests = b.addTest(.{
.root_module = module,
});
const exe_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "web", .module = module },
},
});
const exe = b.addExecutable(.{
.name = "web",
.root_module = exe_module,
});
b.installArtifact(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_tests.step);
}

View File

@@ -0,0 +1,11 @@
.{
.name = .web,
.version = "0.0.0",
.minimum_zig_version = "0.15.2",
.paths = .{
"src",
"build.zig",
"build.zig.zon",
},
.fingerprint = 0x15c938517148d390,
}

9
packages/web/cert.pem Normal file
View File

@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBPDCB76ADAgECAhRuWLL9k0QOIt4+BPNlFBzRcmvREDAFBgMrZXAwFDESMBAG
A1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwODIwNDczNFoXDTM2MDMwNTIwNDczNFow
FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAdgB1CRIUYLCPclWp2+5c
X3I0aqoY7yuhZBE9NxKKbZ+jUzBRMB0GA1UdDgQWBBQXJ3/C7HMVOjXcnqvDKdWo
PWhzsTAfBgNVHSMEGDAWgBQXJ3/C7HMVOjXcnqvDKdWoPWhzsTAPBgNVHRMBAf8E
BTADAQH/MAUGAytlcANBAFirv6F5+TamfddV1sElbfI8jfhPLxWU+z6/mxxbzooX
IKdOh/FrCrPfeUavKH9C5Vr+ztPgoTdCaFqo5mT7MAw=
-----END CERTIFICATE-----

3
packages/web/key.pem Normal file
View File

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIMDUsa31l2xEhX1gyB5w2WABgSbne3GHyUe0RWomq9C5
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,147 @@
const std = @import("std");
const Connection = @This();
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const openssl = @import("openssl.zig");
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
address: std.net.Address,
fd: FileDescriptor,
ssl: ?*openssl.Ssl,
node: std.DoublyLinkedList.Node = .{},
// TODO Consider proper usage of `send` syscall with `MSG_MORE` flag and setting
// the `TCP_CORK` option.
pub fn reinit(
self: *Connection,
address: std.net.Address,
fd: FileDescriptor,
ssl: ?*openssl.Ssl,
) void {
self.address = address;
self.fd = fd;
self.ssl = ssl;
}
pub fn deinit(self: *Connection) void {
if (self.ssl) |x| x.free();
self.fd.close();
self.* = undefined;
}
pub fn read(self: *const Connection, buf: []u8) !usize {
if (self.ssl) |ssl| {
const bytes_read = try ssl.read(buf);
return bytes_read;
} else {
const bytes_read = try self.fd.read(buf);
return bytes_read;
}
}
pub fn readAll(self: *const Connection, buf: []u8) !void {
if (self.ssl) |ssl| {
try ssl.readAll(buf);
} else {
try self.fd.readAll(buf);
}
}
pub fn readv(self: *const Connection, iov: []const iovec) !usize {
if (self.ssl) |ssl| {
var total_bytes_read: usize = 0;
for (iov) |io| {
const bytes_read = try ssl.read(io.base[0..io.len]);
total_bytes_read += bytes_read;
if (bytes_read < io.len) {
return total_bytes_read;
}
}
return total_bytes_read;
} else {
const bytes_read = try self.fd.readv(iov);
return bytes_read;
}
}
/// Might modify `iov` when `readv` syscall reads partially.
pub fn readvAll(self: *const Connection, iov: []iovec) !void {
if (self.ssl) |ssl| {
for (iov) |io| {
try ssl.readAll(io.base[0..io.len]);
}
} else {
try self.fd.readvAll(iov);
}
}
pub fn write(self: *const Connection, buf: []const u8) !usize {
if (self.ssl) |ssl| {
const bytes_written = try ssl.write(buf);
return bytes_written;
} else {
const bytes_written = try self.fd.write(buf);
return bytes_written;
}
}
pub fn writeAll(self: *const Connection, buf: []const u8) !void {
if (self.ssl) |ssl| {
try ssl.writeAll(buf);
} else {
try self.fd.writeAll(buf);
}
}
pub fn writev(self: *const Connection, iov: []const iovec_const) !usize {
if (self.ssl) |ssl| {
var total_bytes_written: usize = 0;
for (iov) |io| {
const bytes_written = try ssl.write(io.base[0..io.len]);
total_bytes_written += bytes_written;
if (bytes_written < io.len) {
return total_bytes_written;
}
}
return total_bytes_written;
} else {
const bytes_written = try self.fd.writev(iov);
return bytes_written;
}
}
/// Might modify `iov` when `writev` syscall writes partially.
pub fn writevAll(self: *const Connection, iov: []iovec_const) !void {
if (self.ssl) |ssl| {
for (iov) |io| {
try ssl.writeAll(io.base[0..io.len]);
}
} else {
try self.fd.writevAll(iov);
}
}
pub fn sendfile(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !usize {
if (self.ssl) |ssl| {
const bytes_written = try ssl.sendfile(fd, offset, len);
return bytes_written;
} else {
const bytes_written = try self.fd.sendfile(fd, offset, len);
return bytes_written;
}
}
pub fn sendfileAll(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !void {
if (self.ssl) |ssl| {
try ssl.sendfileAll(fd, offset, len);
} else {
try self.fd.sendfileAll(fd, offset, len);
}
}

View File

@@ -0,0 +1,238 @@
const std = @import("std");
const linux = std.os.linux;
const errno = linux.E.init;
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
pub const FileDescriptor = enum(i32) {
stdin = 0,
stdout = 1,
stderr = 2,
_,
pub fn memfd_create(name: [*:0]const u8, flags: u32) !FileDescriptor {
const rc = linux.memfd_create(name, flags);
return switch (errno(rc)) {
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
else => error.SystemError,
};
}
pub fn socket(domain: u32, socket_type: u32, protocol: u32) !FileDescriptor {
const rc = linux.socket(domain, socket_type, protocol);
return switch (errno(rc)) {
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
else => error.SystemError,
};
}
pub fn close(self: FileDescriptor) void {
_ = linux.close(@intFromEnum(self));
}
pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor {
while (true) {
const rc = linux.accept(@intFromEnum(self), addr, len);
switch (errno(rc)) {
.SUCCESS => return @enumFromInt(@as(i32, @intCast(rc))),
.INPROGRESS, .AGAIN => continue,
.CONNABORTED => return error.ConnectionAborted,
.PERM => return error.BlockedByFirewall,
else => return error.SystemError,
}
}
}
pub fn bind(self: FileDescriptor, addr: *const linux.sockaddr, len: linux.socklen_t) !void {
const rc = linux.bind(@intFromEnum(self), addr, len);
return switch (errno(rc)) {
.SUCCESS => {},
.ACCES, .PERM => error.AccessDenied,
.ADDRINUSE => error.AddressInUse,
.ADDRNOTAVAIL => error.AddressNotAvailable,
.AFNOSUPPORT => error.AddressFamilyNotSupported,
.LOOP => error.SymLinkLoop,
.NAMETOOLONG => error.NameTooLong,
.NOENT => error.FileNotFound,
.NOTDIR => error.NotDir,
.ROFS => error.ReadOnlyFileSystem,
else => error.SystemError,
};
}
pub fn ftruncate(self: FileDescriptor, length: i64) !void {
const rc = linux.ftruncate(@intFromEnum(self), length);
return switch (errno(rc)) {
.SUCCESS => {},
else => error.SystemError,
};
}
pub fn getsockname(self: FileDescriptor, noalias addr: *linux.sockaddr, noalias len: *linux.socklen_t) !void {
const rc = linux.getsockname(@intFromEnum(self), addr, len);
return switch (errno(rc)) {
.SUCCESS => {},
else => error.SystemError,
};
}
pub fn listen(self: FileDescriptor, backlog: u32) !void {
const rc = linux.listen(@intFromEnum(self), backlog);
return switch (errno(rc)) {
.SUCCESS => {},
.ADDRINUSE => error.AddressInUse,
else => error.SystemError,
};
}
pub fn read(self: FileDescriptor, buf: []u8) !usize {
while (true) {
const rc = linux.read(@intFromEnum(self), buf.ptr, buf.len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
.AGAIN => return error.Timeout,
else => return error.SystemError,
}
}
}
pub fn readAll(self: FileDescriptor, buf: []u8) !void {
var total_bytes_read: usize = 0;
while (total_bytes_read < buf.len) {
const chunk = buf[total_bytes_read..];
const bytes_read = try self.read(chunk);
total_bytes_read += bytes_read;
}
}
pub fn readv(self: FileDescriptor, iov: []const iovec) !usize {
while (true) {
const rc = linux.readv(@intFromEnum(self), iov.ptr, iov.len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
.AGAIN, .WOULDBLOCK => error.Timeout,
else => return error.SystemError,
}
}
}
/// Might modify `iov` when `readv` syscall reads partially.
pub fn readvAll(self: FileDescriptor, iov: []iovec) !void {
var total_bytes_read: usize = 0;
var i: usize = 0;
while (i < iov.len) {
var bytes_read = try self.readv(iov[i..]);
total_bytes_read += bytes_read;
// skip whole buffers
while (i < iov.len and bytes_read >= iov[i].len) {
bytes_read -= iov[i].len;
i += 1;
}
// skip part of a buffer
if (bytes_read > 0) {
iov[i].base += bytes_read;
iov[i].len -= bytes_read;
}
}
}
pub fn setsockopt(self: FileDescriptor, level: i32, optname: u32, opt: []const u8) !void {
const rc = linux.setsockopt(@intFromEnum(self), level, optname, opt.ptr, @intCast(opt.len));
return switch (errno(rc)) {
.SUCCESS => {},
else => error.SystemError,
};
}
pub fn write(self: FileDescriptor, buf: []const u8) !usize {
while (true) {
const rc = linux.write(@intFromEnum(self), buf.ptr, buf.len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
else => return error.SystemError,
}
}
}
pub fn writeAll(self: FileDescriptor, buf: []const u8) !void {
var total_bytes_written: usize = 0;
while (total_bytes_written < buf.len) {
const chunk = buf[total_bytes_written..];
const bytes_written = try self.write(chunk);
total_bytes_written += bytes_written;
}
}
pub fn writev(self: FileDescriptor, iov: []const iovec_const) !usize {
while (true) {
const rc = linux.writev(@intFromEnum(self), iov.ptr, iov.len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
else => return error.SystemError,
}
}
}
/// Might modify `iov` when `writev` syscall writes partially.
pub fn writevAll(self: FileDescriptor, iov: []iovec_const) !void {
var total_bytes_written: usize = 0;
var i: usize = 0;
while (i < iov.len) {
var bytes_written = try self.writev(iov[i..]);
total_bytes_written += bytes_written;
// skip whole buffers
while (i < iov.len and bytes_written >= iov[i].len) {
bytes_written -= iov[i].len;
i += 1;
}
// skip part of a buffer
if (bytes_written > 0) {
iov[i].base += bytes_written;
iov[i].len -= bytes_written;
}
}
}
/// Calls `fstat` and returns total size in bytes.
pub fn size(self: FileDescriptor) !usize {
var stat = std.mem.zeroes(linux.Stat);
const rc = linux.fstat(@intFromEnum(self), &stat);
return switch (errno(rc)) {
.SUCCESS => @intCast(stat.size),
else => error.SystemError,
};
}
pub fn sendfile(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !usize {
var offset_mut = offset;
while (true) {
const rc = linux.sendfile(@intFromEnum(self), @intFromEnum(fd), @ptrCast(&offset_mut), len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
else => return error.SystemError,
}
}
}
pub fn sendfileAll(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !void {
var total_bytes_sent: usize = 0;
while (total_bytes_sent < len) {
const bytes_sent = try self.sendfile(fd, offset + total_bytes_sent, len - total_bytes_sent);
total_bytes_sent += bytes_sent;
}
}
};

41
packages/web/src/Id.zig Normal file
View File

@@ -0,0 +1,41 @@
const std = @import("std");
const UUID = @import("UUID.zig");
const decoder = &std.base64.url_safe_no_pad.Decoder;
const encoder = &std.base64.url_safe_no_pad.Encoder;
pub fn Id(comptime _tag: @Type(.enum_literal)) type {
return struct {
pub const tag = _tag;
uuid: UUID,
pub fn init(uuid: UUID) @This() {
return .{ .uuid = uuid };
}
pub fn eql(a: @This(), b: @This()) bool {
return std.mem.eql(u8, &a.bytes, &b.bytes);
}
pub fn decode(encoded: *const [22]u8) !@This() {
var bytes: [16]u8 = undefined;
decoder.decode(&bytes, encoded) catch |err| switch (err) {
error.InvalidCharacter => return error.InvalidId,
error.InvalidPadding, error.NoSpaceLeft => unreachable,
};
return .{ .bytes = bytes };
}
pub fn encode(self: @This()) [22]u8 {
var text: [22]u8 = undefined;
self.encodeInto(&text);
return text;
}
pub fn encodeInto(self: @This(), text: *[22]u8) void {
encoder.encode(text, self.bytes);
}
};
}

View File

@@ -0,0 +1,66 @@
const std = @import("std");
const Request = @This();
const http = @import("http.zig");
method: http.Method,
pathname: []const u8,
headers: *HeaderHashMap,
body: []const u8,
pub const HeaderHashMap = std.HashMapUnmanaged(
http.FieldName,
HeaderList,
http.FieldName.HashMapContext,
std.hash_map.default_max_load_percentage,
);
pub const HeaderList = struct {
list: std.SinglyLinkedList,
len: usize,
};
pub const HeaderValue = struct {
value: []const u8,
node: std.SinglyLinkedList.Node,
};
/// Gets a header field value of a given `name`. When there is no such header,
/// `null` is returned. When there is more than one header with the same name,
/// the value of the field that came later will be returned.
pub fn getHeader(self: *const Request, name: http.FieldName) ?[]const u8 {
if (self.headers.get(name)) |list| {
if (list.list.first) |head| {
const header_value: *HeaderValue = @fieldParentPtr("node", head);
return header_value.value;
}
}
}
pub fn getHeaderKnown(self: *const Request, known: http.KnownFieldName) ?[]const u8 {
return self.getHeader(.initKnonw(known));
}
pub fn getHeaders(self: *const Request, name: http.FieldName, values: [][]const u8) []const []const u8 {
var i: usize = 0;
var node: ?*std.SinglyLinkedList.Node = if (self.headers.get(name)) |list| list.list else null;
while (i < values.len) {
if (node) |n| {
const header_value: *HeaderValue = @fieldParentPtr("node", n);
values[i] = header_value.value;
i += 1;
node = n.next;
} else {
break;
}
}
return values[0..i];
}
pub fn getHeaderCount(self: *const Request, name: http.FieldName) usize {
const list = self.headers.get(name) orelse return 0;
return list.len;
}

View File

@@ -0,0 +1,16 @@
const std = @import("std");
const RequestHandler = @This();
const Request = @import("Request.zig");
const Response = @import("Response.zig");
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
handle: *const fn (self: *anyopaque, request: *Request, response: *Response) anyerror!void,
};
pub inline fn handle(self: RequestHandler, request: *Request, response: *Response) anyerror!void {
try self.vtable.handle(self.ptr, request, response);
}

View File

@@ -0,0 +1,85 @@
const std = @import("std");
const Response = @This();
const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig");
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
pub const State = union(enum) {
init: void,
sent: void,
errored: anyerror,
pub fn initErrored(err: anyerror) State {
return .{ .errored = err };
}
};
connection: *Connection,
header_writer: std.Io.Writer,
body_writer: std.Io.Writer,
state: State,
pub fn init(connection: *Connection, header_write_buffer: []u8, body_write_buffer: []u8) Response {
return .{
.connection = connection,
.header_writer = .fixed(header_write_buffer),
.body_writer = .fixed(body_write_buffer),
.state = .init,
};
}
pub fn sendHeadersOnly(self: *Response) void {
std.debug.assert(self.state == .init);
const headers_slice = self.header_writer.buffered();
std.debug.assert(headers_slice.len > 0);
if (self.connection.writeAll(headers_slice)) {
self.state = .sent;
} else |err| {
self.state = .initErrored(err);
}
}
pub fn sendHeadersAndBody(self: *Response) void {
std.debug.assert(self.state == .init);
const headers_slice = self.header_writer.buffered();
const body_slice = self.body_writer.buffered();
std.debug.assert(headers_slice.len > 0);
std.debug.assert(body_slice.len > 0);
var iov = [_]iovec_const{
.{ .base = headers_slice.ptr, .len = headers_slice.len },
.{ .base = body_slice.ptr, .len = body_slice.len },
};
if (self.connection.writevAll(&iov)) {
self.state = .sent;
} else |err| {
self.state = .initErrored(err);
}
}
pub fn sendHeadersAndFile(self: *Response, fd: FileDescriptor, offset: usize, maybe_len: ?usize) void {
std.debug.assert(self.state == .init);
const headers_slice = self.header_writer.buffered();
std.debug.assert(headers_slice.len > 0);
const len = maybe_len orelse (try fd.size()) - offset;
if (self.connection.writeAll(headers_slice)) {
if (self.connection.sendfileAll(fd, offset, len)) {
self.state = .sent;
} else |err2| {
self.state = .initErrored(err2);
}
} else |err1| {
self.state = .initErrored(err1);
}
}

375
packages/web/src/Server.zig Normal file
View File

@@ -0,0 +1,375 @@
const std = @import("std");
const Server = @This();
const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig");
const openssl = @import("openssl.zig");
const Request = @import("Request.zig");
const RequestHandler = @import("RequestHandler.zig");
const Worker = @import("Worker.zig");
const log = std.log.scoped(.Server);
const linux = std.os.linux;
const errno = linux.E.init;
fd: FileDescriptor,
address: std.net.Address,
ssl_ctx: ?*openssl.SslContext,
workers: []Worker,
threads: []std.Thread,
request_handler: RequestHandler,
read_timeout_us: u64,
connection_queue: std.DoublyLinkedList,
// NOTE Connection pool has no need for being doubly-linked, but the queue has
// (as it's FIFO) and we want a single intrusive Node to be able to participate
// in both lists. This is possible because a connection will never belong to
// both lists at the same time.
connection_pool: std.DoublyLinkedList,
connection_buffer: []Connection,
mutex: std.Thread.Mutex,
cond_connection_queued: std.Thread.Condition,
cond_connection_freed: std.Thread.Condition,
/// 4 kiB
const page_size = 4 * 1024;
/// 2 MiB
const huge_page_size = 2 * 1024 * 1024;
pub const Options = struct {
request_handler: RequestHandler,
address: std.net.Address = .initIp4(.{ 127, 0, 0, 1 }, 8000),
/// If not `null`, the server will use TLS with the provided OpenSSL
/// context.
ssl_ctx: ?*openssl.SslContext = null,
max_connections: u32 = 128,
/// The number of worker threads. If set to `0`, the number of worker
/// threads will be equal to the number of logical CPU cores.
worker_count: u32 = 0,
/// The maximum number of header fields the `Request` object will be able to
/// store. An HTTP request will be rejected if it has more header fields
/// than the capacity.
max_header_fields: u32 = 256,
/// The number of 2 MiB pages reserved for a single read buffer. Each worker
/// has its own read buffer. An HTTP request (headers and content combined)
/// will be rejected if it is larger than the read buffer.
read_buffer_huge_pages: u32 = 1,
/// The number of 4 kiB pages reserved for a single header write buffer.
/// Each worker has its own header write buffer. The HTTP status line, all
/// header fields and the CRLF terminator must all fit in the header write
/// buffer.
header_write_buffer_pages: u32 = 1,
/// The number of 2 MiB pages reserved for a single body write buffer. Each
/// worker has its own body write buffer. The HTTP response body must fit
/// entirely within the body write buffer. This restriction only applies to
/// bodies generated with the body writer and not to bodies sent with
/// `sendfile`.
body_write_buffer_huge_pages: u32 = 1,
/// How much time should a worker wait on an idle connection before closing
/// it. Specifically, how much time can a `read` syscall block for, before
/// the connection is forcefully closed.
read_timeout_us: u64 = 1 * std.time.us_per_s,
};
pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
const worker_count = if (options.worker_count > 0) options.worker_count else try std.Thread.getCpuCount();
// Create socket fd
const fd: FileDescriptor = try .socket(
options.address.any.family,
linux.SOCK.STREAM | linux.SOCK.CLOEXEC,
if (options.address.any.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP,
);
errdefer fd.close();
const opt = std.mem.toBytes(@as(c_int, 1));
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEADDR, &opt);
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEPORT, &opt);
var socklen = options.address.getOsSockLen();
try fd.bind(&options.address.any, socklen);
try fd.listen(options.max_connections);
var listen_address = options.address;
try fd.getsockname(&listen_address.any, &socklen);
// Allocate arrays
const workers = try allocator.alloc(Worker, worker_count);
errdefer allocator.free(workers);
const connection_buffer = try allocator.alloc(Connection, options.max_connections);
errdefer allocator.free(connection_buffer);
const threads = try allocator.alloc(std.Thread, worker_count);
errdefer allocator.free(threads);
// Allocate and remap read buffers
const single_read_buffer_size = @as(usize, options.read_buffer_huge_pages) * huge_page_size;
const all_read_buffers_size = worker_count * single_read_buffer_size;
const double_single_read_buffer_size = 2 * single_read_buffer_size;
const double_all_read_buffers_size = 2 * all_read_buffers_size;
const read_buffer_fd: FileDescriptor = try .memfd_create("read_buffer", 0);
defer read_buffer_fd.close();
try read_buffer_fd.ftruncate(@intCast(all_read_buffers_size));
const read_buffer_ptr = try errOrPtr(linux.mmap(
null,
double_all_read_buffers_size,
linux.PROT.NONE,
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
));
errdefer _ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size);
_ = linux.madvise(read_buffer_ptr, double_all_read_buffers_size, linux.MADV.HUGEPAGE);
for (0..worker_count) |i| {
const offset = i * single_read_buffer_size;
const double_offset = i * double_single_read_buffer_size;
try err(linux.mmap(
read_buffer_ptr + double_offset,
single_read_buffer_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .SHARED, .FIXED = true },
@intFromEnum(read_buffer_fd),
@intCast(offset),
));
try err(linux.mmap(
read_buffer_ptr + double_offset + single_read_buffer_size,
single_read_buffer_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .SHARED, .FIXED = true },
@intFromEnum(read_buffer_fd),
@intCast(offset),
));
}
// Allocate header write buffer
const single_header_write_buffer_size = @as(usize, options.header_write_buffer_pages) * page_size;
const all_header_write_buffers_size = worker_count * single_header_write_buffer_size;
const header_write_buffer_ptr = try errOrPtr(linux.mmap(
null,
all_header_write_buffers_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
));
errdefer _ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size);
// Allocate body write buffer
const single_body_write_buffer_size = @as(usize, options.body_write_buffer_huge_pages) * huge_page_size;
const all_body_write_buffers_size = worker_count * single_body_write_buffer_size;
const body_write_buffer_ptr = try errOrPtr(linux.mmap(
null,
all_body_write_buffers_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
));
errdefer _ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size);
_ = linux.madvise(body_write_buffer_ptr, all_body_write_buffers_size, linux.MADV.HUGEPAGE);
// Initialize workers
var workers_initialized: usize = 0;
errdefer {
for (workers[0..workers_initialized]) |*worker| {
worker.deinit(allocator);
}
}
for (workers, 0..) |*worker, i| {
const read_offset = i * double_single_read_buffer_size;
const header_write_offset = i * single_header_write_buffer_size;
const body_write_offset = i * single_body_write_buffer_size;
worker.* = try Worker.init(allocator, .{
.worker_id = i,
.max_header_fields = options.max_header_fields,
.read_buffer_ptr = read_buffer_ptr + read_offset,
.read_buffer_size = single_read_buffer_size,
.header_write_buffer = (header_write_buffer_ptr + header_write_offset)[0..single_header_write_buffer_size],
.body_write_buffer = (body_write_buffer_ptr + body_write_offset)[0..single_body_write_buffer_size],
});
workers_initialized += 1;
}
// Fill connection pool
var connection_pool: std.DoublyLinkedList = .{};
for (connection_buffer) |*c| {
connection_pool.prepend(&c.node);
}
return .{
.fd = fd,
.address = listen_address,
.ssl_ctx = options.ssl_ctx,
.workers = workers,
.threads = threads,
.request_handler = options.request_handler,
.read_timeout_us = options.read_timeout_us,
.connection_queue = .{},
.connection_pool = connection_pool,
.connection_buffer = connection_buffer,
.mutex = .{},
.cond_connection_queued = .{},
.cond_connection_freed = .{},
};
}
pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
log.debug("Deinitializing Server.", .{});
const worker_count = self.workers.len;
const single_read_buffers_size = self.workers[0].read_buffer_size;
const all_read_buffers_size = worker_count * single_read_buffers_size;
const double_all_read_buffers_size = 2 * all_read_buffers_size;
const single_header_write_buffer_size = self.workers[0].header_write_buffer.len;
const all_header_write_buffers_size = worker_count * single_header_write_buffer_size;
const single_body_write_buffer_size = self.workers[0].body_write_buffer.len;
const all_body_write_buffers_size = worker_count * single_body_write_buffer_size;
const read_buffer_ptr = self.workers[0].read_buffer_ptr;
const header_write_buffer_ptr = self.workers[0].header_write_buffer.ptr;
const body_write_buffer_ptr = self.workers[0].body_write_buffer.ptr;
for (self.workers) |*worker| {
worker.deinit(allocator);
}
_ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size);
_ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size);
_ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size);
allocator.free(self.threads);
allocator.free(self.connection_buffer);
allocator.free(self.workers);
self.fd.close();
self.* = undefined;
}
/// This method block until the server is stopped, which is achieved by storing
/// `false` into `running`. You should use another thread or interruption
/// handler to be able to stop the server.
pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
var worker_running: std.atomic.Value(bool) = .init(running.load(.acquire));
var spawned: usize = 0;
defer {
log.debug("Storing `false` into worker_running.", .{});
worker_running.store(false, .release);
log.debug("Broadcasting connection queued condition variable.", .{});
self.cond_connection_queued.broadcast();
for (self.threads[0..spawned], 0..) |*thread, i| {
log.debug("Joining the thread of worker #{d}.", .{i});
thread.join();
}
}
for (self.workers, 0..) |*worker, i| {
log.debug("Spawning thread for worker #{d}.", .{i});
self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
spawned += 1;
}
while (running.load(.acquire)) {
var address: std.net.Address = undefined;
var address_size: u32 = @sizeOf(std.net.Address);
log.debug("Accepting connection.", .{});
const fd = self.fd.accept(&address.any, &address_size) catch |e| {
log.err("Error while accepting connection: {}", .{e});
continue;
};
log.debug("Accepted connection from {f}", .{address});
const timeout: linux.timeval = .{
.sec = @intCast(self.read_timeout_us / std.time.us_per_s),
.usec = @intCast(self.read_timeout_us % std.time.us_per_s),
};
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.RCVTIMEO, std.mem.asBytes(&timeout));
const ssl: ?*openssl.Ssl = self.maybeInitSsl(fd) catch |e| {
log.err("Error while estabilishing SSL connection: {}", .{e});
fd.close();
continue;
};
{
log.debug("Acquiring mutex.", .{});
self.mutex.lock();
log.debug("Acquired mutex.", .{});
defer {
log.debug("Unlocking mutex.", .{});
self.mutex.unlock();
}
while (true) {
if (self.connection_pool.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node);
connection.reinit(address, fd, ssl);
log.debug("Adding connection to {f} to the connection queue.", .{connection.address});
self.connection_queue.prepend(node);
break;
}
log.debug("Waiting on connection freed condition variable.", .{});
self.cond_connection_freed.wait(&self.mutex);
log.debug("Woken up on connection freed condition variable.", .{});
}
}
log.debug("Signaling connection queued condition variable.", .{});
self.cond_connection_queued.signal();
} else {
log.debug("Loaded `false` from running, the accept loop exited.", .{});
}
}
fn maybeInitSsl(self: *const Server, fd: FileDescriptor) !?*openssl.Ssl {
if (self.ssl_ctx) |ssl_ctx| {
const ssl = try openssl.Ssl.new(ssl_ctx);
try ssl.setFd(fd);
log.debug("Accepting SSL layer.", .{});
try ssl.accept();
log.debug("Accepted SSL layer.", .{});
return ssl;
} else {
return null;
}
}
fn err(rc: usize) !void {
const e = errno(rc);
return if (e != .SUCCESS) error.SystemError else {};
}
fn errOrPtr(rc: usize) ![*]u8 {
const e = errno(rc);
return if (e != .SUCCESS) error.SystemError else @ptrFromInt(rc);
}

87
packages/web/src/UUID.zig Normal file
View File

@@ -0,0 +1,87 @@
const std = @import("std");
const UUID = @This();
bytes: [16]u8,
pub const zero: UUID = .{ .bytes = @splat(0) };
pub fn v4() UUID {
var bytes: [16]u8 = undefined;
std.crypto.random.bytes(&bytes);
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
return .{ .bytes = bytes };
}
pub const namespaces = struct {
/// Name string is a fully-qualified domain name
///
/// `6ba7b810-9dad-11d1-80b4-00c04fd430c8`
pub const dns: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x10, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
/// Name string is a URL
///
/// `6ba7b811-9dad-11d1-80b4-00c04fd430c8`
pub const url: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x11, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
/// Name string is an ISO OID
///
/// `6ba7b812-9dad-11d1-80b4-00c04fd430c8`
pub const oid: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x12, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
/// Name string is an X.500 DN (in DER or a text output format)
///
/// `6ba7b814-9dad-11d1-80b4-00c04fd430c8`
pub const x500: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x14, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
};
pub fn v5(namespace: UUID, name: []const u8) UUID {
const hash = blk: {
var hasher = std.crypto.hash.Sha1.init(.{});
hasher.update(namespace);
hasher.update(name);
break :blk hasher.finalResult();
};
var bytes: [16]u8 = hash[0..16].*;
bytes[6] = (bytes[6] & 0x0F) | 0x50;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
return .{ .bytes = bytes };
}
var v7_lock: std.Thread.Mutex = .{};
var v7_last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
var v7_counter: std.atomic.Value(u32) = .{ .raw = 0 };
fn getCount(timestamp: u64) u32 {
v7_lock.lock();
defer v7_lock.unlock();
if (v7_last_timestamp.swap(timestamp, .monotonic) != timestamp) {
v7_counter.store(0, .monotonic);
}
return v7_counter.fetchAdd(1, .monotonic) % 4096;
}
pub fn v7() UUID {
const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp()));
const count = getCount(timestamp);
const random = blk: {
var bytes: [8]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var bytes: [16]u8 = undefined;
bytes[0] = @truncate(timestamp >> 40);
bytes[1] = @truncate(timestamp >> 32);
bytes[2] = @truncate(timestamp >> 24);
bytes[3] = @truncate(timestamp >> 16);
bytes[4] = @truncate(timestamp >> 8);
bytes[5] = @truncate(timestamp);
bytes[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
bytes[7] = @truncate(count);
bytes[8] = (random[0] & 0x3F) | 0x80;
@memcpy(bytes[9..16], random[1..8]);
return .{ .bytes = bytes };
}

321
packages/web/src/Worker.zig Normal file
View File

@@ -0,0 +1,321 @@
const std = @import("std");
const Worker = @This();
const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig");
const Request = @import("Request.zig");
const RequestHandler = @import("RequestHandler.zig");
const Response = @import("Response.zig");
const Server = @import("Server.zig");
const log = std.log.scoped(.Worker);
/// Integer unique for this worker. Has no functional meaning. Can be used for
/// debugging and profiling.
worker_id: usize,
read_buffer_ptr: [*]u8,
read_buffer_size: usize,
read_head: usize,
read_tail: usize,
header_write_buffer: []u8,
body_write_buffer: []u8,
header_hash_map: Request.HeaderHashMap,
header_value_buffer: []Request.HeaderValue,
pub const Options = struct {
worker_id: usize,
max_header_fields: u32,
read_buffer_ptr: [*]u8,
read_buffer_size: usize,
header_write_buffer: []u8,
body_write_buffer: []u8,
};
pub fn init(allocator: std.mem.Allocator, options: Options) !Worker {
var header_hash_map: Request.HeaderHashMap = .empty;
try header_hash_map.ensureTotalCapacity(allocator, options.max_header_fields);
errdefer header_hash_map.deinit(allocator);
const header_value_buffer = try allocator.alloc(Request.HeaderValue, options.max_header_fields);
errdefer allocator.free(header_value_buffer);
return .{
.worker_id = options.worker_id,
.read_buffer_ptr = options.read_buffer_ptr,
.read_buffer_size = options.read_buffer_size,
.read_head = 0,
.read_tail = 0,
.header_write_buffer = options.header_write_buffer,
.body_write_buffer = options.body_write_buffer,
.header_hash_map = header_hash_map,
.header_value_buffer = header_value_buffer,
};
}
pub fn deinit(self: *Worker, allocator: std.mem.Allocator) void {
log.debug("[#{d}] Deinitializing Worker.", .{self.worker_id});
allocator.free(self.header_value_buffer);
self.header_hash_map.deinit(allocator);
self.* = undefined;
}
pub fn worker(
self: *Worker,
server: *Server,
running: *const std.atomic.Value(bool),
) void {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock();
log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
defer {
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
server.mutex.unlock();
}
while (running.load(.acquire)) {
if (server.connection_queue.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node);
log.debug("[#{d}] Popped connection to {f} from the connection queue.", .{ self.worker_id, connection.address });
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
server.mutex.unlock();
defer {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock();
log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
log.debug("[#{d}] Returning connection to connection pool.", .{self.worker_id});
server.connection_pool.append(&connection.node);
log.debug("[#{d}] Signaling connection freed condition variable.", .{self.worker_id});
server.cond_connection_freed.signal();
}
log.debug("[#{d}] Handling connection to {f}.", .{ self.worker_id, connection.address });
self.handleConnection(server.request_handler, connection, running) catch |err| {
log.err("[#{d}] Error while handling connection: {}", .{ self.worker_id, err });
};
} else {
log.debug("[#{d}] Waiting on connection queued condition variable.", .{self.worker_id});
server.cond_connection_queued.wait(&server.mutex);
log.debug("[#{d}] Woken up on connection queued condition variable.", .{self.worker_id});
}
} else {
log.debug("[#{d}] Loaded `false` from running, the worker loop exited.", .{self.worker_id});
}
}
fn handleConnection(
self: *Worker,
request_handler: RequestHandler,
connection: *Connection,
running: *const std.atomic.Value(bool),
) !void {
defer connection.deinit();
while (running.load(.acquire)) {
const res = self.handleRequest(request_handler, connection) catch |err| {
log.err("[#{d}] Error while handling request: {}", .{ self.worker_id, err });
return err;
};
if (!res) {
log.debug("[#{d}] Request handler indicated to stop handling the connection to {f}.", .{ self.worker_id, connection.address });
break;
}
} else {
log.debug("[#{d}] Loaded `false` from running, the connection handler loop exited.", .{self.worker_id});
}
}
fn handleRequest(
self: *Worker,
request_handler: RequestHandler,
connection: *Connection,
) !bool {
self.header_hash_map.clearRetainingCapacity();
var request: Request = .{
.method = undefined,
.pathname = undefined,
.headers = &self.header_hash_map,
.body = undefined,
};
var response: Response = .init(connection, self.header_write_buffer, self.body_write_buffer);
var parser: http.Parser = .init();
var next_header_index: usize = 0;
var ignore: bool = false;
var client_closed: bool = false;
var leftover_bytes = self.read_tail - self.read_head;
const max_read_tail = self.read_head + self.read_buffer_size;
while (true) {
var bytes_read: usize = undefined;
var chunk: []const u8 = undefined;
if (leftover_bytes > 0) {
bytes_read = leftover_bytes;
chunk = self.read_buffer_ptr[self.read_tail - leftover_bytes .. self.read_tail];
leftover_bytes = 0;
} else {
const read_tail = self.read_tail;
bytes_read = connection.read(self.read_buffer_ptr[read_tail..max_read_tail]) catch |err| switch (err) {
error.Timeout => {
log.debug("[#{d}] Connection to {f} timed out.", .{ self.worker_id, connection.address });
return false;
},
else => return err,
};
if (bytes_read == 0) {
log.debug("[#{d}] Read zero bytes from connection to {f}.", .{ self.worker_id, connection.address });
return false;
}
chunk = self.read_buffer_ptr[read_tail .. read_tail + bytes_read];
self.read_tail += bytes_read;
}
const res = parser.consume(chunk) catch |err| {
switch (err) {
error.MethodNotSupported => try closeWith(&response, http.status.method_not_allowed),
error.HttpVersionNotSupported => try closeWith(&response, http.status.http_version_not_supported),
error.SyntaxError => try closeWith(&response, http.status.bad_request),
}
return false;
};
const done = if (res.result) |result| std.meta.activeTag(result) == .body else false;
if (self.read_tail - self.read_head >= self.read_buffer_size and !done) {
if (parser.state == .body) {
try closeWith(&response, http.status.content_too_large);
} else {
try closeWith(&response, http.status.request_header_fields_too_large);
}
return false;
}
if (res.result) |result| {
switch (result) {
.method => |method| request.method = method,
.pathname => |pathname| request.pathname = pathname,
.header => |header| blk: {
if (header.isNamedKnown(.Connection) and std.mem.eql(u8, header.value, "close")) {
client_closed = true;
}
if (ignore) {
break :blk;
}
if (next_header_index >= self.header_value_buffer.len or self.header_hash_map.available == 0) {
// TODO Here, we could ignore, but make sure this does
// not clash with the other "request too long" checks
// (i.e. be careful not to double respond).
_ = &ignore;
try closeWith(&response, http.status.request_header_fields_too_large);
return false;
} else {
const entry = self.header_hash_map.getOrPutAssumeCapacity(header.name);
const header_value = &self.header_value_buffer[next_header_index];
header_value.* = .{ .node = .{}, .value = header.value };
next_header_index += 1;
if (!entry.found_existing) {
entry.value_ptr.* = .{
.len = 0,
.list = .{},
};
}
entry.value_ptr.list.prepend(&header_value.node);
entry.value_ptr.len += 1;
}
},
.end_of_headers => {},
.body => |body| {
request.body = body;
if (!ignore) {
request_handler.handle(&request, &response) catch |err| {
if (response.state == .init) {
response.header_writer.end = 0;
response.body_writer.end = 0;
const error_name = @errorName(err);
try response.body_writer.print("Internal Server Error\n{s}\n", .{error_name});
try response.header_writer.writeAll(http.status.internal_server_error);
try response.header_writer.writeAll("Content-Type: text/plain; charset=utf-8\r\n");
try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end});
try response.header_writer.writeAll("\r\n");
response.sendHeadersAndBody();
}
};
}
if (response.state == .init) {
const no_headers = response.header_writer.end > 0;
const no_body = response.body_writer.end > 0;
if (no_headers) {
if (no_body) {
try response.header_writer.writeAll(http.status.no_content);
try response.header_writer.writeAll("\r\n");
response.sendHeadersOnly();
} else {
try response.header_writer.writeAll(http.status.ok);
try response.header_writer.writeAll("Content-Type: application/octet-stream");
try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end});
try response.header_writer.writeAll("\r\n");
response.sendHeadersAndBody();
}
} else {
if (no_body) {
response.sendHeadersOnly();
} else {
response.sendHeadersAndBody();
}
}
}
leftover_bytes = bytes_read - res.consumed;
self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1);
self.read_tail = self.read_head + leftover_bytes;
return !client_closed;
},
}
}
leftover_bytes = bytes_read - res.consumed;
}
}
fn closeWith(response: *Response, status_line: []const u8) !void {
// This function is meant to be called before a request handler gets to do
// anything.
std.debug.assert(response.header_writer.end == 0);
std.debug.assert(response.body_writer.end == 0);
std.debug.assert(response.state == .init);
try response.header_writer.writeAll(status_line);
try response.header_writer.writeAll("Connection: close\r\n");
try response.header_writer.writeAll("\r\n");
}

View File

@@ -0,0 +1,8 @@
const std = @import("std");
pub const FieldName = @import("http/FieldName.zig").FieldName;
pub const Header = @import("http/Header.zig");
pub const KnownFieldName = @import("http/KnownFieldName.zig").KnownFieldName;
pub const Method = @import("http/Method.zig").Method;
pub const Parser = @import("http/Parser.zig");
pub const status = @import("http/status.zig");

Some files were not shown because too many files have changed in this diff Show More