summaryrefslogtreecommitdiff
path: root/src/net/libc.zig
blob: 38eab6d64292b53e928169be792c2cf0a85f3152 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! The libc symbols ESP-Hosted's C reaches for, and nothing more.
//!
//! This is not a libc. It is the exact set measured by linking the transport, and each entry is here
//! because a specific call site needs it:
//!
//!   nm on the milestone-1 objects (transport_drv.o transport_util.o sdio_drv.o mempool.o) leaves
//!   26 undefined symbols. These are the libc ones: memcpy memset strcpy snprintf __errno_location
//!   htole16 le16toh, plus malloc/free/realloc once mempool.c is included.
//!
//! Two sources cover them:
//!
//!  1. compiler_rt, which Zig links automatically. It provides the memory primitives - memcpy,
//!     memset, memcmp, memmove - and the integer helpers clang emits for 64-bit division on a
//!     32-bit target, __udivdi3 and __divdi3. It provides no `str*` functions at all.
//!  2. This file, for everything else.
//!
//! The P4 mask ROM is a third possibility that this project deliberately does not use yet.
//! `components/esp_rom/esp32p4/ld/esp32p4.rom.newlib.ld` exports 32 newlib symbols - strlen,
//! strlcpy, strchr, strstr, memset, qsort, atoi and friends - as absolute addresses, which would
//! cost no code in the image and would be the same implementation IDF links. It is not wired in
//! because that file assigns those names unconditionally rather than with PROVIDE, so it would
//! collide with compiler_rt's own memset and memcpy definitions. Trading a real duplicate-symbol
//! hazard for a few hundred bytes is not worth it while the image is 2 KB.
//!
//! Deliberately absent: stdio beyond snprintf, locale, floating-point formatting beyond what
//! std.fmt gives, and anything reentrant. If a link error names a symbol not here, the honest move
//! is to add it here with a comment saying which call site wanted it - not to link a real libc.

const std = @import("std");

/// Set by `install`. ESP-Hosted allocates per-packet buffers and frees them, so this cannot be an
/// arena; see the allocator discussion in src/net/port.zig.
var gpa: ?std.mem.Allocator = null;

pub fn install(allocator: std.mem.Allocator) void {
    gpa = allocator;
}

// ---------------------------------------------------------------------------------------------
// malloc family
//
// C's `free` carries no size, but Zig's Allocator.free needs one. The classic fix is a header word
// in front of every block holding the length. It costs 8 bytes per allocation (the word plus
// padding to keep the payload 8-aligned, which the SDIO IDMAC path needs anyway) and it is the only
// way to bridge the two contracts without a side table.
// ---------------------------------------------------------------------------------------------

/// Payload alignment. 8 rather than 4 because DMA descriptors on this chip want 8-byte alignment,
/// and buffers handed to CMD53 come from here.
const malloc_align: std.mem.Alignment = .@"8";
const header_size = malloc_align.toByteUnits();

comptime {
    // The header must not push the payload out of alignment.
    std.debug.assert(header_size >= @sizeOf(usize));
    std.debug.assert(header_size % malloc_align.toByteUnits() == 0);
}

fn allocBlock(total_payload: usize) ?[*]u8 {
    const a = gpa orelse @panic("libc malloc before install()");
    const raw = a.rawAlloc(header_size + total_payload, malloc_align, @returnAddress()) orelse
        return null;
    // Record the payload length in the word directly before the payload.
    const payload = raw + header_size;
    @as(*usize, @ptrCast(@alignCast(raw))).* = total_payload;
    return payload;
}

fn payloadLen(payload: [*]u8) usize {
    return @as(*const usize, @ptrCast(@alignCast(payload - header_size))).*;
}

fn freeBlock(payload: [*]u8) void {
    const a = gpa orelse @panic("libc free before install()");
    const len = payloadLen(payload);
    a.rawFree((payload - header_size)[0 .. header_size + len], malloc_align, @returnAddress());
}

export fn malloc(size: usize) callconv(.c) ?*anyopaque {
    if (size == 0) return null;
    return @ptrCast(allocBlock(size));
}

export fn calloc(n: usize, size: usize) callconv(.c) ?*anyopaque {
    const total = std.math.mul(usize, n, size) catch return null;
    if (total == 0) return null;
    const p = allocBlock(total) orelse return null;
    @memset(p[0..total], 0);
    return @ptrCast(p);
}

export fn free(ptr: ?*anyopaque) callconv(.c) void {
    const p = ptr orelse return;
    freeBlock(@ptrCast(p));
}

export fn realloc(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {
    const p = ptr orelse return malloc(size);
    if (size == 0) {
        freeBlock(@ptrCast(p));
        return null;
    }
    const old: [*]u8 = @ptrCast(p);
    const old_len = payloadLen(old);
    if (old_len == size) return ptr;

    // Try to grow or shrink in place first; the allocator may well be able to, and mempool.c
    // reallocs the same buffer repeatedly.
    const a = gpa orelse @panic("libc realloc before install()");
    const whole = (old - header_size)[0 .. header_size + old_len];
    if (a.rawResize(whole, malloc_align, header_size + size, @returnAddress())) {
        @as(*usize, @ptrCast(@alignCast(old - header_size))).* = size;
        return ptr;
    }

    const new = allocBlock(size) orelse return null;
    @memcpy(new[0..@min(old_len, size)], old[0..@min(old_len, size)]);
    freeBlock(old);
    return @ptrCast(new);
}

/// ESP-Hosted's `_h_malloc_align` path and IDF's `heap_caps_aligned_alloc` both land here. The
/// header trick still works as long as the requested alignment is not stricter than ours; anything
/// stricter would need the payload moved and the header written at a computed offset, and nothing
/// in the measured surface asks for that. Assert rather than silently misalign a DMA buffer.
export fn aligned_alloc(alignment: usize, size: usize) callconv(.c) ?*anyopaque {
    // A stricter alignment would need the payload moved and the header written at a computed
    // offset. Nothing in the measured surface asks for it, so this asserts rather than silently
    // handing back a misaligned DMA buffer - which would corrupt a packet, not fail a call.
    if (alignment > malloc_align.toByteUnits()) @panic("aligned_alloc: alignment stricter than 8");
    return malloc(size);
}

// ---------------------------------------------------------------------------------------------
// string
//
// compiler_rt covers `mem*` and nothing else, so every `str*` ESP-Hosted references is here. The
// list is exactly what the link demanded - measured, not anticipated.
// ---------------------------------------------------------------------------------------------

export fn strlen(s: [*:0]const u8) callconv(.c) usize {
    // std.mem.len is the same loop; going through it keeps this honest about being a wrapper rather
    // than a hand-optimised copy of something the standard library already has.
    return std.mem.len(s);
}

export fn strcpy(dst: [*]u8, src: [*:0]const u8) callconv(.c) [*]u8 {
    var i: usize = 0;
    while (src[i] != 0) : (i += 1) dst[i] = src[i];
    dst[i] = 0;
    return dst;
}

export fn strnlen(s: [*]const u8, max: usize) callconv(.c) usize {
    var i: usize = 0;
    while (i < max and s[i] != 0) : (i += 1) {}
    return i;
}

export fn strcmp(a: [*:0]const u8, b: [*:0]const u8) callconv(.c) c_int {
    var i: usize = 0;
    while (a[i] != 0 and a[i] == b[i]) : (i += 1) {}
    return @as(c_int, a[i]) - @as(c_int, b[i]);
}

export fn strncmp(a: [*]const u8, b: [*]const u8, n: usize) callconv(.c) c_int {
    var i: usize = 0;
    while (i < n) : (i += 1) {
        if (a[i] != b[i]) return @as(c_int, a[i]) - @as(c_int, b[i]);
        if (a[i] == 0) break;
    }
    return 0;
}

// ---------------------------------------------------------------------------------------------
// endian helpers
//
// These are macros in musl's <endian.h>, but ESP-Hosted takes their address in a couple of places,
// so clang emits calls and the linker wants real symbols. riscv32 is little-endian, so both are
// identity - which is exactly why getting them wrong would be invisible here and corrupt on a
// big-endian host. Written as byte-order conversions rather than `return x` to say so.
// ---------------------------------------------------------------------------------------------

export fn htole16(x: u16) callconv(.c) u16 {
    return std.mem.nativeToLittle(u16, x);
}

export fn le16toh(x: u16) callconv(.c) u16 {
    return std.mem.littleToNative(u16, x);
}

export fn htole32(x: u32) callconv(.c) u32 {
    return std.mem.nativeToLittle(u32, x);
}

export fn le32toh(x: u32) callconv(.c) u32 {
    return std.mem.littleToNative(u32, x);
}

// ---------------------------------------------------------------------------------------------
// errno
//
// ESP-Hosted reads errno after its own calls fail. There are no threads competing for it in a
// cooperative runtime, so one global is correct here; it would need to be per-task the moment a
// preemptive scheduler appeared.
// ---------------------------------------------------------------------------------------------

var errno_storage: c_int = 0;

export fn __errno_location() callconv(.c) *c_int {
    return &errno_storage;
}

// ---------------------------------------------------------------------------------------------
// snprintf
//
// The one genuinely non-trivial entry. ESP-Hosted uses it for log lines and for formatting MAC
// addresses and transport state, so the conversions that matter are %d %u %x %s %c %p and width /
// zero-pad on the integer ones. std.fmt does the formatting; this only parses the C format string.
//
// Unsupported conversions print `%!` followed by the specifier rather than being skipped, so a
// format this does not handle is visible in the log instead of silently dropping its argument.
// ---------------------------------------------------------------------------------------------

export fn snprintf(buf: [*]u8, size: usize, fmt: [*:0]const u8, ...) callconv(.c) c_int {
    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    return vsnprintfImpl(buf, size, fmt, &ap);
}

export fn vsnprintf(
    buf: [*]u8,
    size: usize,
    fmt: [*:0]const u8,
    ap: *std.builtin.VaList,
) callconv(.c) c_int {
    return vsnprintfImpl(buf, size, fmt, ap);
}

/// `callconv(.c)` is required, not stylistic: `@cVaArg` is only available in a function using the C
/// calling convention, and Zig rejects it in an `auto` one.
fn vsnprintfImpl(
    buf: [*]u8,
    size: usize,
    fmt: [*:0]const u8,
    ap: *std.builtin.VaList,
) callconv(.c) c_int {
    // Writes into the caller's buffer, tracking how many bytes *would* have been written, because
    // that is what snprintf returns and callers use it to size a second call.
    var out: Counting = .{ .buf = if (size == 0) &.{} else buf[0 .. size - 1] };

    var i: usize = 0;
    while (fmt[i] != 0) : (i += 1) {
        if (fmt[i] != '%') {
            out.byte(fmt[i]);
            continue;
        }
        i += 1;
        if (fmt[i] == '%') {
            out.byte('%');
            continue;
        }

        // flags and width: only the subset ESP-Hosted uses
        var zero_pad = false;
        var width: usize = 0;
        while (fmt[i] == '0' or fmt[i] == '-' or fmt[i] == '+' or fmt[i] == ' ') : (i += 1) {
            if (fmt[i] == '0') zero_pad = true;
        }
        while (fmt[i] >= '1' and fmt[i] <= '9') : (i += 1) {
            width = width * 10 + (fmt[i] - '0');
        }
        // length modifiers: consumed, and `ll`/`z` widen the fetch below
        var long_long = false;
        while (true) : (i += 1) {
            switch (fmt[i]) {
                'l' => if (fmt[i + 1] == 'l') {
                    long_long = true;
                } else {},
                'h', 'z', 't', 'j' => {},
                else => break,
            }
        }

        switch (fmt[i]) {
            'd', 'i' => {
                if (long_long) {
                    out.int(@cVaArg(ap, i64), 10, false, width, zero_pad);
                } else {
                    out.int(@cVaArg(ap, c_int), 10, false, width, zero_pad);
                }
            },
            'u' => {
                if (long_long) {
                    out.int(@cVaArg(ap, u64), 10, false, width, zero_pad);
                } else {
                    out.int(@cVaArg(ap, c_uint), 10, false, width, zero_pad);
                }
            },
            'x' => out.int(@cVaArg(ap, c_uint), 16, false, width, zero_pad),
            'X' => out.int(@cVaArg(ap, c_uint), 16, true, width, zero_pad),
            'c' => out.byte(@truncate(@as(c_uint, @bitCast(@cVaArg(ap, c_int))))),
            's' => {
                const s = @cVaArg(ap, ?[*:0]const u8) orelse "(null)";
                var n: usize = 0;
                while (s[n] != 0) : (n += 1) {}
                out.pad(width, n, ' ');
                out.slice(s[0..n]);
            },
            'p' => {
                out.slice("0x");
                out.int(@intFromPtr(@cVaArg(ap, ?*anyopaque)), 16, false, 8, true);
            },
            0 => break,
            else => {
                // Unsupported: say so in the output rather than desynchronising silently. The
                // argument is deliberately not consumed - there is no way to know its width.
                out.slice("%!");
                out.byte(fmt[i]);
            },
        }
    }

    if (size != 0) buf[@min(out.written, size - 1)] = 0;
    return @intCast(out.would);
}

/// A writer that stops filling at the end of the buffer but keeps counting, which is what
/// snprintf's return value means.
const Counting = struct {
    buf: []u8,
    written: usize = 0,
    would: usize = 0,

    fn byte(self: *Counting, c: u8) void {
        if (self.written < self.buf.len) {
            self.buf[self.written] = c;
            self.written += 1;
        }
        self.would += 1;
    }

    fn slice(self: *Counting, s: []const u8) void {
        for (s) |c| self.byte(c);
    }

    fn pad(self: *Counting, width: usize, len: usize, fill: u8) void {
        if (width > len) for (0..width - len) |_| self.byte(fill);
    }

    fn int(
        self: *Counting,
        value: anytype,
        base: u8,
        upper: bool,
        width: usize,
        zero_pad: bool,
    ) void {
        var tmp: [24]u8 = undefined;
        const end = std.fmt.printInt(&tmp, value, base, if (upper) .upper else .lower, .{});
        const s = tmp[0..end];
        self.pad(width, s.len, if (zero_pad) '0' else ' ');
        self.slice(s);
    }
};

// ---------------------------------------------------------------------------------------------
// Tests. These run on the host, where a wrong snprintf is cheap to find; on the die it would be a
// garbled log line at best and a buffer overrun at worst.
// ---------------------------------------------------------------------------------------------

test "snprintf: the conversions esp_hosted actually uses" {
    var buf: [64]u8 = undefined;
    const n = snprintf(&buf, buf.len, "state %d port %u flags 0x%x %s", @as(c_int, -3), @as(c_uint, 7), @as(c_uint, 0xbeef), "ok");
    try std.testing.expectEqualStrings("state -3 port 7 flags 0xbeef ok", buf[0..@intCast(n)]);
}

test "snprintf: return value is the length that would have been written" {
    var buf: [8]u8 = undefined;
    const n = snprintf(&buf, buf.len, "%s", "0123456789");
    // Truncated to 7 chars plus NUL, but reports the full 10 so a caller can size a second call.
    try std.testing.expectEqual(@as(c_int, 10), n);
    try std.testing.expectEqualStrings("0123456", buf[0..7]);
    try std.testing.expectEqual(@as(u8, 0), buf[7]);
}

test "snprintf: zero-padded width, as used for MAC bytes" {
    var buf: [32]u8 = undefined;
    const n = snprintf(&buf, buf.len, "%02x:%02x", @as(c_uint, 0x0a), @as(c_uint, 0xf1));
    try std.testing.expectEqualStrings("0a:f1", buf[0..@intCast(n)]);
}

test "snprintf: size 0 writes nothing at all" {
    var buf = [_]u8{0xAA} ** 4;
    const n = snprintf(&buf, 0, "hello");
    try std.testing.expectEqual(@as(c_int, 5), n);
    try std.testing.expectEqual(@as(u8, 0xAA), buf[0]);
}

test "snprintf: an unsupported conversion is visible, not silent" {
    var buf: [32]u8 = undefined;
    const n = snprintf(&buf, buf.len, "f=%f", @as(f64, 1.5));
    try std.testing.expectEqualStrings("f=%!f", buf[0..@intCast(n)]);
}

test "malloc/free/realloc survive the churn mempool.c generates" {
    var backing: [4096]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&backing);
    install(fba.allocator());
    defer gpa = null;

    // Same-size alloc/free churn: the case an arena cannot serve.
    var i: usize = 0;
    while (i < 8) : (i += 1) {
        const p = malloc(64) orelse return error.OutOfMemory;
        free(p);
    }

    const a = malloc(32) orelse return error.OutOfMemory;
    @memset(@as([*]u8, @ptrCast(a))[0..32], 0x5A);
    const b = realloc(a, 64) orelse return error.OutOfMemory;
    // Contents must survive the grow.
    try std.testing.expectEqual(@as(u8, 0x5A), @as([*]u8, @ptrCast(b))[31]);
    free(b);
}

test "calloc zeroes, and rejects overflow rather than under-allocating" {
    var backing: [1024]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&backing);
    install(fba.allocator());
    defer gpa = null;

    const p = calloc(16, 4) orelse return error.OutOfMemory;
    for (@as([*]u8, @ptrCast(p))[0..64]) |byte| try std.testing.expectEqual(@as(u8, 0), byte);
    free(p);

    try std.testing.expect(calloc(std.math.maxInt(usize), 2) == null);
}

test "strlen, strcpy, strcmp and strncmp agree with std" {
    try std.testing.expectEqual(@as(usize, 0), strlen(""));
    try std.testing.expectEqual(@as(usize, 3), strlen("abc"));
    // strnlen stops at the bound, which is the whole reason the shim uses it on wire data.
    try std.testing.expectEqual(@as(usize, 3), strnlen("abc", 8));
    try std.testing.expectEqual(@as(usize, 2), strnlen("abc", 2));
    try std.testing.expectEqual(@as(usize, 0), strnlen("abc", 0));

    var dst: [8]u8 = undefined;
    _ = strcpy(&dst, "abc");
    try std.testing.expectEqualStrings("abc", dst[0..3]);
    try std.testing.expectEqual(@as(u8, 0), dst[3]);

    try std.testing.expect(strcmp("abc", "abc") == 0);
    try std.testing.expect(strcmp("abc", "abd") < 0);
    try std.testing.expect(strncmp("abcX", "abcY", 3) == 0);
    try std.testing.expect(strncmp("abcX", "abcY", 4) != 0);
}