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
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
|
//! The seam: ESP-Hosted's station data channel, bridged to `src/net/ip.zig`.
//!
//! Everything below this file is proven - the SDIO host driver, the runtime, the port table, the
//! RPC layer, the association. Everything above it is proven too: `ip.zig` has 117 host tests and a
//! mutation sweep. This file is the twenty lines of pointer handling in between, and it is the one
//! part of the path that no host test can check, because both of its neighbours are C.
//!
//! So every decision here is cited rather than inferred.
//!
//! ------------------------------------------------------------------------------------------
//! 1. Where the received frame starts: at `buffer`, offset zero.
//!
//! This is the single most expensive thing to get wrong. A frame shifted by the 12-byte
//! `esp_payload_header` parses as garbage - the ethertype lands in the middle of a MAC address -
//! and every one of ip.zig's tests would still pass. The RX convention is established by the
//! producer and confirmed by the vendor's own consumer:
//!
//! * sdio_drv.c:830 rejects any packet whose header `offset` field is not
//! `sizeof(struct esp_payload_header)`, so the payload always begins exactly one header in.
//! * sdio_drv.c:887 `buf_handle.payload = rxbuff + offset` - `payload` already points past the
//! header. `priv_buffer_handle` (:882) is what still points at the header.
//! * sdio_drv.c:1396-1400 allocates `copy_payload = _h_malloc(buf_handle->payload_len)` and
//! memcpy's `payload_len` bytes from `buf_handle->payload` into it, then frees the original
//! buffer at :1401. So the copy is exactly the payload, nothing more.
//! * sdio_drv.c:1407-1408 `rx(api_chan, copy_payload, copy_payload, payload_len)` - `buffer`
//! and `buff_to_free` are the same pointer, and it is the start of the frame.
//! * The vendor's own consumer agrees: esp_wifi_remote_net2.c:40-51 passes `buffer` straight to
//! the netif receive function as the frame and `buff_to_free` only as the free handle.
//!
//! `H_ESP_PAYLOAD_HEADER_OFFSET` appears on the *transmit* side only (transport_drv.c:381), where
//! ESP-Hosted is *building* a buffer and has to leave room for the header it is about to write.
//! Adding it on receive would be applying the same correction twice, in the wrong direction.
//!
//! ------------------------------------------------------------------------------------------
//! 2. Who frees, and with what.
//!
//! `copy_payload` came from `_h_malloc` (sdio_drv.c:1396), so it is freed with `_h_free` - which
//! is exactly what `HOSTED_FREE` expands to (port_esp_hosted_host_os.h:139) and what
//! `transport_sta_free_cb` reaches through `MEMPOOL_FREE` with the pool disabled
//! (transport_util.h:29-31). `onRxFrame` below frees it through `g_h.funcs->_h_free`, once, on
//! every path including the error paths, and always returns `ESP_OK`.
//!
//! Returning `ESP_OK` unconditionally is not laziness, it is the only value that is safe under
//! both of sdio_drv.c's ownership rules. With `ESP_WIFI_REMOTE_VERSION` >= 1.3.1 the callee always
//! owns the buffer and the caller never frees (:1418). Below that, and when the macro is undefined,
//! the caller frees the buffer *if the callee returned non-zero* (:1411-1416). A non-zero return
//! from a callback that has already freed is therefore a double free under one rule and a leak
//! under neither - so this file frees and returns zero, which is one free under both.
//!
//! ------------------------------------------------------------------------------------------
//! 3. `api_chan` must not be null.
//!
//! `transport_drv_sta_tx` opens with `assert(h && h == chan_arr[ESP_STA_IF]->api_chan)`
//! (transport_drv.c:369), and the vendor's reference RX callback opens with `assert(h)`
//! (esp_wifi_remote_net2.c:41). ESP-Hosted's own registration honours that: it allocates a cookie
//! and passes it in (esp_hosted_api.c:200-203). This build compiles the C at -O2 with `-DNDEBUG`
//! (Zig adds it for every non-Debug optimize mode), so those asserts are compiled out today and a
//! null cookie would merely be an unchecked contract violation rather than a crash - which is a
//! worse outcome, not a better one. `channel_cookie` below is that non-null cookie, and it is
//! handed back to `tx` on every transmit so the identity check holds.
//!
//! ------------------------------------------------------------------------------------------
//! 4. The transmitted frame need not outlive the call.
//!
//! `transport_drv_sta_tx` allocates its own buffer and copies into it before queueing:
//! `mempool_alloc(..., MAX_TRANSPORT_BUFFER_SIZE, true)` at transport_drv.c:372 - with the pool
//! disabled that is `_h_malloc_align(1536, 64)` (transport_util.h:21-27) - then
//! `_h_memcpy(copy_buff + H_ESP_PAYLOAD_HEADER_OFFSET, buffer, len)` at :381, and only then
//! `esp_hosted_tx(..., copy_buff, ...)` at :383. Nothing retains `buffer`. That is what makes
//! `ip.Stack`'s "the slice is borrowed for the duration of the call" contract satisfiable, and it
//! is why `sendFrame` may hand over a pointer into the stack's single transmit staging buffer.
//!
//! ------------------------------------------------------------------------------------------
//! 5. Why there is a re-entrancy guard.
//!
//! This is the one hazard the task description does not mention and it is real.
//!
//! `ip.Stack` is a single-threaded state machine: `onFrame` may send (an ARP reply, an ICMP echo
//! reply, a TCP ACK) before it returns, and `tick` and `httpGet` may too. Sending ends in
//! `esp_hosted_tx`, whose last act is
//! `_h_queue_item(to_slave_queue[prio], &buf_handle, HOSTED_BLOCK_MAX)` (sdio_drv.c:1607). That
//! queue holds four items (`CONFIG_ESP_HOSTED_SDIO_TX_Q_SIZE 4`, src/net/hosted/sdkconfig.h:41)
//! and `_h_queue_item` with `HOSTED_BLOCK_MAX` is a *blocking* send: port.zig:734-740 forwards it
//! to `os.Queue.send`, which suspends the calling task until there is room.
//!
//! So a full transmit queue suspends whoever is inside the stack. `onRxFrame` runs on ESP-Hosted's
//! `sdio_process_rx_task`; `tick` and `httpGet` run on the application's task. Without a guard,
//! either one can be suspended mid-mutation and the other walk straight into the same `Stack`.
//! On a cooperative scheduler that is not a torn read, it is two interleaved state machines
//! sharing one transmit buffer, one TCP sequence space and one `http.out` slice.
//!
//! The guard makes that impossible, and every way it can fire has a correct answer already:
//!
//! * a frame arriving while the stack is busy is dropped, which is what a real NIC does when its
//! transmit queue is full. DHCP, ARP and TCP all retransmit.
//! * a `tick` skipped is a `tick` deferred: `ip.zig`'s timers are absolute deadlines compared
//! against `now_ms` (`dhcpTick`, `tcpTick`), not increments, so nothing is lost.
//! * `httpGet` returns `error.WouldBlock`, which is precisely the answer its protocol already
//! requires the caller to handle by calling again with identical arguments.
//!
//! Each of those is counted, so a log can say which one happened rather than leaving a stall
//! unexplained.
const std = @import("std");
const ip = @import("ip.zig");
const port = @import("port.zig");
// ================================================================= ESP-Hosted's C surface
/// `esp_hosted_if_type_t`, common/esp_hosted_interface.h:14-24.
///
/// Note the value. The enumeration opens with `ESP_INVALID_IF`, so the station interface is **1**,
/// not 0. Registering channel 0 would fall through `transport_drv_add_channel`'s switch to
/// `default:` (transport_drv.c:481-484), which logs "Not yet supported" and returns NULL after
/// having already installed a half-built channel - and `chan_arr[ESP_STA_IF]` would stay NULL, so
/// sdio_drv.c:1394 would go on discarding every station frame in silence.
const esp_sta_if: c_uint = 1;
/// `transport_channel_tx_fn_t`, transport_drv.h:118. Returns `esp_err_t`; 0 is `ESP_OK`.
const TxFn = *const fn (h: ?*anyopaque, buffer: ?*anyopaque, len: usize) callconv(.c) c_int;
/// `transport_channel_rx_fn_t`, transport_drv.h:119.
const RxFn = *const fn (
h: ?*anyopaque,
buffer: ?*anyopaque,
buff_to_free: ?*anyopaque,
len: usize,
) callconv(.c) c_int;
/// transport_drv.h:134-136. `tx` is an out-parameter: the transport writes the interface's own
/// transmit function into it (transport_drv.c:469-471) and that is the only way to obtain it.
///
/// This is compiled in - `transport_drv.c` is on build.zig's source list - but nothing calls it,
/// because the file that normally does (`esp_hosted_api.c`'s `add_esp_wifi_remote_channels`) is
/// not compiled: this project calls `setup_transport`, `rpc_init` and `transport_drv_reconfigure`
/// directly from `src/net/all.zig`. Registering the station channel is therefore ours to do.
extern fn transport_drv_add_channel(
api_chan: ?*anyopaque,
if_type: c_uint,
secure: u8,
tx: *?TxFn,
rx: RxFn,
) ?*anyopaque;
/// The station's MAC, through the C shim (src/net/hosted/wifi_shim.c:96). It belongs to the C6's
/// radio, not to this chip, and ARP and Ethernet framing are built on it. Valid only after
/// `hosted_wifi_sta_start`, because that is what brings the radio up on the coprocessor.
extern fn hosted_wifi_get_mac(out: *[6]u8) c_int;
// ============================================================================== module state
/// The one IPv4 stack. A module-level variable rather than something the caller owns, because
/// `ip.Stack.send` is `*const fn ([]const u8) void` with no context pointer: the transmit callback
/// has to reach the transport some other way, and a file-scope binding is the honest version of
/// "some other way". 3,576 bytes of .bss - see `footprint`.
var sta: ip.Stack = undefined;
/// The `api_chan` cookie. Its address is what ESP-Hosted stores and compares; its contents are
/// never read by anyone. See note 3 in the header for why it may not be null.
var channel_cookie: u32 = 0x5354_4100; // 'STA\0', so a memory dump names it
/// The transport's station transmit function, from `transport_drv_add_channel`'s out-parameter.
var tx_fn: ?TxFn = null;
/// Set once the channel is registered and the stack is live.
var opened: bool = false;
/// The re-entrancy guard. See note 5 in the header.
var in_stack: bool = false;
pub const Stats = struct {
/// Frames handed to us by sdio_drv.c, before any filtering.
rx_frames: u32 = 0,
/// Frames whose `h` was not our cookie. Non-zero means another channel's traffic reached this
/// callback, which would be an ESP-Hosted bug and not something to paper over.
rx_wrong_channel: u32 = 0,
/// `buffer` was null, or `len` was zero or larger than an Ethernet frame.
rx_bad: u32 = 0,
/// Frames dropped because the stack was already entered. See note 5.
rx_reentrant: u32 = 0,
/// Frames actually delivered to `ip.Stack.onFrame`.
rx_delivered: u32 = 0,
/// `tick` calls that found the stack entered and did nothing.
tick_skipped: u32 = 0,
/// `httpGet`/`httpGetHost` calls answered `WouldBlock` by the guard rather than by the stack.
http_deferred: u32 = 0,
/// `resolve` calls answered `WouldBlock` by the guard rather than by the stack. The query's
/// own timer runs in `tick`, so these cost a poll and never a retransmission.
dns_deferred: u32 = 0,
/// Frames handed to the transport.
tx_frames: u32 = 0,
/// Transmits the transport rejected: not ready, throttled, or out of buffers.
tx_failed: u32 = 0,
/// Transmits attempted before the channel existed. Should be zero.
tx_no_channel: u32 = 0,
/// Frames the stack asked to send, accepted into the deferred ring. The difference between this
/// and `tx_frames` is what is still waiting for the next `tick`.
tx_queued: u32 = 0,
/// Frames dropped because the deferred ring was full when the stack tried to send. Non-zero
/// means `tick` is not keeping up with the offered load; every protocol above this retransmits,
/// so it costs latency rather than correctness.
tx_ring_full: u32 = 0,
/// Frames the stack offered with an impossible length. Should be zero; a non-zero value points
/// at ip.zig rather than at the transport.
tx_bad: u32 = 0,
};
var counters: Stats = .{};
/// Everything this file adds to .bss, so the number in a report cannot rot. The stack dominates it.
pub const footprint: usize =
@sizeOf(@TypeOf(sta)) +
@sizeOf(@TypeOf(channel_cookie)) +
@sizeOf(@TypeOf(tx_fn)) +
@sizeOf(@TypeOf(opened)) +
@sizeOf(@TypeOf(in_stack)) +
@sizeOf(@TypeOf(counters)) +
@sizeOf(@TypeOf(tx_ring));
// ================================================================================= transmit
/// `ip.Stack.send`. The slice is borrowed for the duration of this call only, which is exactly what
/// the transport needs - see note 4 in the header.
fn sendFrame(frame: []const u8) void {
if (tx_fn == null) {
counters.tx_no_channel += 1;
return;
}
if (frame.len == 0 or frame.len > ip.frame_max) {
counters.tx_bad += 1;
return;
}
// Queued, never transmitted from here. See `flushTx`.
const next = (tx_ring.head + 1) % tx_ring_slots;
if (next == tx_ring.tail) {
counters.tx_ring_full += 1;
return;
}
@memcpy(tx_ring.slot[tx_ring.head][0..frame.len], frame);
tx_ring.len[tx_ring.head] = @intCast(frame.len);
tx_ring.head = next;
counters.tx_queued += 1;
}
/// Hand every queued frame to ESP-Hosted. MUST be called only from a task that may block.
///
/// This indirection is the fix for a deadlock the board demonstrated, and it is worth stating
/// exactly because the shape of it is not obvious.
///
/// `ip.Stack.onFrame` answers things: an ARP request gets a reply, an ICMP echo gets an echo, a TCP
/// segment gets an ACK. So a received frame turns into a transmitted frame inside `onFrame`. But
/// `onFrame` runs on ESP-Hosted's `sdio_process_rx_task`, and transmitting ends in
/// `_h_queue_item(to_slave_queue, HOSTED_BLOCK_MAX)` (sdio_drv.c:1607), which SUSPENDS the caller
/// when the queue is full. Suspend the RX task and it stops draining the receive queue; the receive
/// queue fills; ESP-Hosted logs "task still writing Rx data to queue!" and stops delivering.
/// Everything then looks like a dead IP stack.
///
/// Measured on the board before this change: frames received froze at 17 and never advanced again,
/// no ping was ever answered, and the HTTP GET failed with HostUnreachable because the ARP reply it
/// needed was never sent. Raising the SDIO queue depth from 4 to 16 only moved the number.
///
/// So the receive path now only ever copies into this ring, which cannot block, and the application
/// task drains it from `tick`. The cost is one copy and `tx_ring_slots * frame_max` of .bss.
fn flushTx() void {
const tx = tx_fn orelse return;
while (tx_ring.tail != tx_ring.head) {
const i = tx_ring.tail;
const n = tx_ring.len[i];
counters.tx_frames += 1;
// The const cast is sound and it is load-bearing that it is: `transport_drv_sta_tx` reads
// `buffer` exactly once, as the source of a memcpy into its own aligned buffer
// (transport_drv.c:381), and neither writes through it nor retains it. ESP-Hosted's
// signature is simply not const-correct.
const rc = tx(@ptrCast(&channel_cookie), @ptrCast(&tx_ring.slot[i]), n);
if (rc != 0) counters.tx_failed += 1;
// Advance only after the call returns, so a frame is never handed out twice.
tx_ring.tail = (i + 1) % tx_ring_slots;
}
}
/// Outgoing frames waiting for a task that may block.
///
/// Four slots, at `ip.frame_max` each. Enough that the replies one pass of received frames can
/// generate - an ARP answer, an ICMP echo, a TCP ACK - all fit, since the whole ring is drained on
/// the very next `tick`. A full ring drops the newest frame and counts it, which is what a real
/// network interface does under load, and every protocol above this retransmits.
///
/// Deliberately small: this is .bss competing with the heap ESP-Hosted allocates every received
/// frame from, and eight slots cost 12 KB that the transport needs more than this ring does.
const tx_ring_slots = 4;
var tx_ring: struct {
slot: [tx_ring_slots][ip.frame_max]u8 = undefined,
len: [tx_ring_slots]u16 = @splat(0),
head: usize = 0,
tail: usize = 0,
} = .{};
// ================================================================================== receive
/// `transport_channel_rx_fn_t`. Called from ESP-Hosted's `sdio_process_rx_task`
/// (sdio_drv.c:1407), which is one of the tasks `port.zig` spawned on this project's own runtime.
///
/// The buffer is ours the moment this is entered, and it is freed on every path. See notes 1 and 2.
fn onRxFrame(
h: ?*anyopaque,
buffer: ?*anyopaque,
buff_to_free: ?*anyopaque,
len: usize,
) callconv(.c) c_int {
// `HOSTED_FREE(buff)` is `g_h.funcs->_h_free(buff)` (port_esp_hosted_host_os.h:139), and this
// is that call. First statement in the function so that no early return can miss it: the
// failure mode of a missed free here is not a leak that shows up in a heap report, it is the
// 32 KiB heap exhausted in a few seconds of the AP's broadcast traffic.
defer port.g_h.funcs.free(buff_to_free);
counters.rx_frames += 1;
if (h != @as(?*anyopaque, @ptrCast(&channel_cookie))) {
counters.rx_wrong_channel += 1;
return 0;
}
const bytes: [*]const u8 = @ptrCast(buffer orelse {
counters.rx_bad += 1;
return 0;
});
if (!opened or len == 0 or len > ip.frame_max) {
counters.rx_bad += 1;
return 0;
}
if (in_stack) {
counters.rx_reentrant += 1;
return 0;
}
in_stack = true;
defer in_stack = false;
counters.rx_delivered += 1;
sta.onFrame(bytes[0..len]);
return 0;
}
// ================================================================================ lifecycle
pub const Error = error{
/// `hosted_wifi_get_mac` failed, or answered with the all-zero MAC that means "no radio yet".
/// The usual cause is calling this before `hosted_wifi_sta_start`.
MacUnavailable,
/// `transport_drv_add_channel` refused, or accepted without filling in the transmit function.
ChannelRegisterFailed,
AlreadyOpen,
};
/// Register the station channel and bring the IP stack up behind it.
///
/// Call after `net.init` and after `hosted_wifi_sta_start`; association may follow or may already
/// have happened, it makes no difference to this. Registering *before* associating is the tidier
/// order, because `chan_arr[ESP_STA_IF]` becoming non-null is the moment sdio_drv.c stops
/// discarding station frames, and until then a live association fills ESP-Hosted's receive queue
/// and logs "task still writing Rx data to queue!".
///
/// The order inside matters: the stack is constructed *before* the channel is registered. The
/// instant `transport_drv_add_channel` returns, `sdio_process_rx_task` may call `onRxFrame`, and
/// that must not find `sta` uninitialised.
pub fn open() Error!void {
if (opened) return error.AlreadyOpen;
var mac_bytes: [6]u8 = @splat(0);
if (hosted_wifi_get_mac(&mac_bytes) != 0) return error.MacUnavailable;
// An all-zero MAC is not a MAC. It is what the shim hands back if the coprocessor answered
// without having a station interface, and building an ARP cache on it would produce a stack
// that transmits frames no switch will ever route back.
if (std.mem.allEqual(u8, &mac_bytes, 0)) return error.MacUnavailable;
sta = .init(mac_bytes, &sendFrame);
var tx: ?TxFn = null;
const channel = transport_drv_add_channel(
@ptrCast(&channel_cookie),
esp_sta_if,
0, // secure=0: plain text, as ESP-Hosted itself uses for the two Wi-Fi interfaces
// (esp_hosted_api.c:105-107). The secure path is the RPC channel's, and RPC has
// its own already.
&tx,
&onRxFrame,
);
if (channel == null) return error.ChannelRegisterFailed;
// Belt and braces: the switch at transport_drv.c:467-485 is the only writer of `*tx`, and the
// one branch that leaves it untouched also returns NULL. Checking both means a future
// ESP-Hosted that separates those cannot leave us with a live channel and no way to transmit.
tx_fn = tx orelse return error.ChannelRegisterFailed;
opened = true;
}
/// True once `open` has succeeded.
pub fn isOpen() bool {
return opened;
}
// ============================================================ the guarded entry points
//
// Every function that can mutate the stack goes through `in_stack`. Every function that only reads
// it does not, because a read cannot suspend and the worst it can observe is a value one frame out
// of date.
/// Advance the stack's clock. Returns false if the stack was busy and the tick was skipped, which
/// is harmless - see note 5 - but worth being able to see.
pub fn tick(now_ms: u64) bool {
if (in_stack) {
counters.tick_skipped += 1;
return false;
}
in_stack = true;
sta.tick(now_ms);
in_stack = false;
// Outside the guard, and last: draining may block, and `in_stack` must not be held across a
// suspension or the receive path would drop every frame that arrived while we waited.
flushTx();
return true;
}
/// Begin DHCP. Call `tick` at least once first: `dhcpStart` stamps the acquisition's start time
/// from the stack's idea of now, which only `tick` sets. Returns false if the stack was busy.
pub fn dhcpStart() bool {
if (in_stack) return false;
in_stack = true;
defer in_stack = false;
sta.dhcpStart();
return true;
}
/// Configure statically instead of asking a server.
pub fn setStatic(addr: [4]u8, mask: [4]u8, gw: [4]u8) bool {
if (in_stack) return false;
in_stack = true;
defer in_stack = false;
sta.setStatic(addr, mask, gw);
return true;
}
/// Override the resolver `resolve` asks. Not needed on a network whose DHCP server offers one -
/// `dhcpBind` stores option 6 and `resolve` uses it with no configuration at all. Returns false if
/// the stack was busy.
pub fn setDnsServer(addr: [4]u8) bool {
if (in_stack) return false;
in_stack = true;
defer in_stack = false;
sta.setDnsServer(addr);
return true;
}
/// One HTTP GET, with the address literal as the `Host:` header. `ip.Stack.httpGet`'s protocol,
/// unchanged: this returns `error.WouldBlock` until the body is complete, and the caller must keep
/// calling with *identical* arguments while driving `tick`. `out` is borrowed until a length comes
/// back.
pub fn httpGet(host: [4]u8, remote_port: u16, path: []const u8, out: []u8) ip.HttpError!usize {
return httpGetHost(host, null, remote_port, path, out);
}
/// The same, with an explicit `Host:` name for a name-based virtual host. See
/// `ip.Stack.httpGetHost`; `name` is part of the request's identity, so it must not change between
/// calls any more than `path` may.
pub fn httpGetHost(
host: [4]u8,
name: ?[]const u8,
remote_port: u16,
path: []const u8,
out: []u8,
) ip.HttpError!usize {
if (in_stack) {
// Answering the caller's own protocol back at it. The alternative - waiting - would be a
// second place in this file that can block, and the guard exists to have exactly none.
counters.http_deferred += 1;
return error.WouldBlock;
}
in_stack = true;
defer in_stack = false;
return sta.httpGetHost(host, name, remote_port, path, out);
}
/// Resolve a name to an address. `ip.Stack.resolve`'s protocol, which is `httpGet`'s: this returns
/// `error.WouldBlock` until an address or a real error comes back, and the caller keeps calling
/// with the same name while driving `tick`.
///
/// The guard's answer is the same `error.WouldBlock`, for the same reason it is in `httpGetHost`:
/// the query's own retransmissions run in `sta.tick`, so a deferred poll costs nothing and the 7 s
/// bound still holds. Frames the query sends go through `sendFrame` into the deferred ring like
/// every other frame here - nothing on this path touches the transport's tx function directly.
pub fn resolve(name: []const u8) ip.DnsError!ip.Ip4 {
if (in_stack) {
counters.dns_deferred += 1;
return error.WouldBlock;
}
in_stack = true;
defer in_stack = false;
return sta.resolve(name);
}
// ==================================================================== read-only accessors
/// The station MAC the stack was built on.
pub fn mac() [6]u8 {
return sta.mac;
}
/// The configured address, or null if there is none yet.
pub fn address() ?[4]u8 {
return sta.addr;
}
pub fn netmask() [4]u8 {
return sta.mask;
}
pub fn gateway() [4]u8 {
return sta.gw;
}
pub fn dnsServer() ?[4]u8 {
return sta.dns;
}
pub fn dhcpState() ip.DhcpState {
return sta.dhcp.state;
}
pub fn tcpState() ip.TcpState {
return sta.tcp.state;
}
pub fn httpStatus() u16 {
return sta.http.status;
}
/// The IP stack's own counters: frames in, frames dropped, echoes answered, checksums rejected.
pub fn ipCounters() ip.Counters {
return sta.counters;
}
/// This file's counters: the transport boundary, and every way the guard fired.
pub fn stats() Stats {
return counters;
}
// There are no tests here, and that is an answer rather than an omission. Two of the three things
// this file does are calls into ESP-Hosted's C - `transport_drv_add_channel` and the transmit
// function it hands back - and the third is a callback that C invokes. A host test could only
// exercise it against a mock of the very code whose conventions are the thing in doubt, and it
// would pass just as happily against a mock that put the frame one header too late. The evidence
// that matters is the citations in this file's header and a board that answers a ping.
|