Skip to content

Commit dd63c87

Browse files
authored
feat: support any writer (#33)
1 parent f382648 commit dd63c87

10 files changed

Lines changed: 161 additions & 145 deletions

File tree

README.org

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#+TITLE: zig-curl
22
#+DATE: 2023-09-16T23:16:15+0800
3-
#+LASTMOD: 2025-07-27T20:02:08+0800
3+
#+LASTMOD: 2025-08-02T11:33:35+0800
44
#+OPTIONS: toc:nil num:nil
55
#+STARTUP: content
66

@@ -44,13 +44,15 @@ pub fn main() !void {
4444
});
4545
defer easy.deinit();
4646

47-
var ctx = curl.ResizableWriteContext.init(allocator);
48-
defer ctx.deinit();
49-
const resp = try easy.fetch("https://httpbin.org/anything", .{}, &ctx);
47+
var writer = curl.ResizableResponseWriter.init(allocator);
48+
defer writer.deinit();
49+
const resp = try easy.fetch("https://httpbin.org/anything", .{
50+
.response_writer = writer.asAny(),
51+
});
5052

5153
std.debug.print("Status code: {d}\nBody: {s}\n", .{
5254
resp.status_code,
53-
ctx.asSlice(),
55+
writer.asSlice(),
5456
});
5557
}
5658
#+end_src

examples/advanced.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,20 @@ fn putWithCustomHeader(allocator: Allocator, easy: Easy) !void {
4141
try easy.setVerbose(true);
4242
try easy.setPostFields(body);
4343

44-
var writeContext = curl.ResizableWriteContext.init(allocator);
45-
defer writeContext.deinit();
46-
try easy.setWriteContext(&writeContext, curl.ResizableWriteContext.write);
44+
var writer = curl.ResizableResponseWriter.init(allocator);
45+
defer writer.deinit();
46+
try easy.setAnyWriter(&writer.asAny());
4747

4848
const resp = try easy.perform();
4949
std.debug.print("Status code: {d}\nBody: {s}\n", .{
5050
resp.status_code,
51-
writeContext.asSlice(),
51+
writer.asSlice(),
5252
});
5353

5454
const parsed = try std.json.parseFromSlice(
5555
Response,
5656
allocator,
57-
writeContext.asSlice(),
57+
writer.asSlice(),
5858
.{ .ignore_unknown_fields = true },
5959
);
6060
defer parsed.deinit();
@@ -103,12 +103,12 @@ fn postMultiPart(allocator: Allocator, easy: Easy) !void {
103103
try easy.setMultiPart(multi_part);
104104
try easy.setVerbose(true);
105105

106-
var writeContext = curl.ResizableWriteContext.init(allocator);
107-
defer writeContext.deinit();
108-
try easy.setWriteContext(&writeContext, curl.ResizableWriteContext.write);
106+
var writer = curl.ResizableResponseWriter.init(allocator);
107+
defer writer.deinit();
108+
try easy.setAnyWriter(&writer.asAny());
109109

110110
const resp = try easy.perform();
111-
std.debug.print("code: {d}, resp:{s}\n", .{ resp.status_code, writeContext.asSlice() });
111+
std.debug.print("code: {d}, resp:{s}\n", .{ resp.status_code, writer.asSlice() });
112112
}
113113

114114
pub fn main() !void {

examples/basic.zig

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ const LOCAL_SERVER_ADDR = "http://localhost:8182";
1010
fn get(easy: Easy) !void {
1111
{
1212
println("GET without write context");
13-
const resp = try easy.fetch("https://httpbin.org/anything", .{}, {});
13+
const resp = try easy.fetch("https://httpbin.org/anything", .{});
1414

1515
std.debug.print("Status code: {d}\n", .{resp.status_code});
1616
}
1717

1818
{
1919
println("GET with fixed buffer");
2020
var buffer: [1024]u8 = undefined;
21-
var writeContext = curl.FixedWriteContext.init(&buffer);
22-
const resp = try easy.fetch("https://httpbin.org/anything", .{}, &writeContext);
21+
var writer = curl.FixedResponseWriter.init(&buffer);
22+
const resp = try easy.fetch("https://httpbin.org/anything", .{
23+
.response_writer = writer.asAny(),
24+
});
2325
std.debug.print("Status code: {d}\nBody: {s}\n", .{
2426
resp.status_code,
25-
writeContext.asSlice(),
27+
writer.asSlice(),
2628
});
2729
}
2830
}
@@ -31,8 +33,8 @@ fn post(allocator: Allocator, easy: Easy) !void {
3133
const payload =
3234
\\{"name": "John", "age": 15}
3335
;
34-
var writeContext = curl.ResizableWriteContext.init(allocator);
35-
defer writeContext.deinit();
36+
var writer = curl.ResizableResponseWriter.init(allocator);
37+
defer writer.deinit();
3638
const resp = try easy.fetch(
3739
"https://httpbin.org/anything",
3840
.{
@@ -41,26 +43,26 @@ fn post(allocator: Allocator, easy: Easy) !void {
4143
.headers = &.{
4244
"Content-Type: application/json",
4345
},
46+
.response_writer = writer.asAny(),
4447
},
45-
&writeContext,
4648
);
4749

4850
std.debug.print("Status code: {d}\nBody: {s}\n", .{
4951
resp.status_code,
50-
writeContext.asSlice(),
52+
writer.asSlice(),
5153
});
5254
}
5355

5456
fn upload(allocator: Allocator, easy: Easy) !void {
5557
const path = "LICENSE";
56-
var writeContext = curl.ResizableWriteContext.init(allocator);
57-
defer writeContext.deinit();
58+
var writer = curl.ResizableResponseWriter.init(allocator);
59+
defer writer.deinit();
5860

59-
const resp = try easy.upload(LOCAL_SERVER_ADDR ++ "/anything", path, &writeContext);
61+
const resp = try easy.upload(LOCAL_SERVER_ADDR ++ "/anything", path, writer.asAny());
6062

6163
std.debug.print("Status code: {d}\nBody: {s}\n", .{
6264
resp.status_code,
63-
writeContext.asSlice(),
65+
writer.asSlice(),
6466
});
6567
}
6668

examples/header.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ fn iterateHeaders(easy: Easy) !void {
77
// Reset old options, e.g. headers.
88
easy.reset();
99

10-
const resp = try easy.fetch("https://httpbin.org/response-headers?X-Foo=1&X-Foo=2&X-Foo=3", .{}, {});
10+
const resp = try easy.fetch("https://httpbin.org/response-headers?X-Foo=1&X-Foo=2&X-Foo=3", .{});
1111

1212
std.debug.print("Iterating all headers...\n", .{});
1313
{
@@ -31,7 +31,7 @@ fn iterateHeaders(easy: Easy) !void {
3131

3232
fn iterateRedirectedHeaders(easy: Easy) !void {
3333
try easy.setFollowLocation(true);
34-
const resp = try easy.fetch("https://httpbin.org/redirect/1", .{}, {});
34+
const resp = try easy.fetch("https://httpbin.org/redirect/1", .{});
3535

3636
const redirects = try resp.getRedirectCount();
3737
try std.testing.expectEqual(redirects, 1);

examples/hello.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ pub fn main() !void {
1313
});
1414
defer easy.deinit();
1515

16-
var ctx = curl.ResizableWriteContext.init(allocator);
17-
defer ctx.deinit();
18-
const resp = try easy.fetch("https://httpbin.org/anything", .{}, &ctx);
16+
var writer = curl.ResizableResponseWriter.init(allocator);
17+
defer writer.deinit();
18+
const resp = try easy.fetch("https://httpbin.org/anything", .{
19+
.response_writer = writer.asAny(),
20+
});
1921

2022
std.debug.print("Status code: {d}\nBody: {s}\n", .{
2123
resp.status_code,
22-
ctx.asSlice(),
24+
writer.asSlice(),
2325
});
2426
}

examples/multi.zig

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ const Easy = curl.Easy;
77
const Multi = curl.Multi;
88
const c = curl.libcurl;
99
const checkCode = curl.checkCode;
10+
const AnyWriter = std.io.AnyWriter;
1011

11-
fn newEasy(ctx: *curl.ResizableWriteContext, url: [:0]const u8) !Easy {
12+
fn newEasy(writer: *curl.ResizableResponseWriter, any_writer: *const AnyWriter, url: [:0]const u8) !Easy {
1213
const easy = try Easy.init(.{});
1314
try easy.setUrl(url);
14-
try easy.setWriteContext(ctx, curl.ResizableWriteContext.write);
15+
try easy.setAnyWriter(any_writer);
1516
// CURLOPT_PRIVATE allows us to store a pointer to the ctx in the easy handle
1617
// so we can retrieve it later in the callback.
17-
try easy.setPrivate(ctx);
18+
try easy.setPrivate(writer);
1819

1920
return easy;
2021
}
@@ -27,13 +28,15 @@ pub fn main() !void {
2728
const multi = try Multi.init();
2829
defer multi.deinit();
2930

30-
var ctx1 = curl.ResizableWriteContext.init(allocator);
31-
defer ctx1.deinit();
32-
var ctx2 = curl.ResizableWriteContext.init(allocator);
33-
defer ctx2.deinit();
31+
var wtr1 = curl.ResizableResponseWriter.init(allocator);
32+
defer wtr1.deinit();
33+
const any_writer1: AnyWriter = wtr1.asAny();
34+
var wtr2 = curl.ResizableResponseWriter.init(allocator);
35+
defer wtr2.deinit();
36+
const any_writer2: AnyWriter = wtr2.asAny();
3437

35-
try multi.addHandle(try newEasy(&ctx1, "http://httpbin.org/headers"));
36-
try multi.addHandle(try newEasy(&ctx2, "http://httpbin.org/ip"));
38+
try multi.addHandle(try newEasy(&wtr1, &any_writer1, "http://httpbin.org/headers"));
39+
try multi.addHandle(try newEasy(&wtr2, &any_writer2, "http://httpbin.org/ip"));
3740

3841
var keep_running = true;
3942
while (keep_running) {
@@ -69,8 +72,8 @@ pub fn main() !void {
6972
// Get the private data (buffer) associated with this handle
7073
var private_data: ?*anyopaque = null;
7174
try checkCode(c.curl_easy_getinfo(easy_handle, c.CURLINFO_PRIVATE, &private_data));
72-
const ctx: *curl.ResizableWriteContext = @ptrCast(@alignCast(private_data.?));
75+
const writer: *curl.ResizableResponseWriter = @ptrCast(@alignCast(private_data.?));
7376

74-
std.debug.print("Response body: {s}\n", .{ctx.asSlice()});
77+
std.debug.print("Response body: {s}\n", .{writer.asSlice()});
7578
}
7679
}

src/Easy.zig

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
const std = @import("std");
22
const errors = @import("errors.zig");
33
const util = @import("util.zig");
4+
const types = @import("types.zig");
45
const c = util.c;
56

67
const mem = std.mem;
78
const fmt = std.fmt;
9+
const AnyWriter = std.io.AnyWriter;
810
const Allocator = mem.Allocator;
911
const checkCode = errors.checkCode;
10-
const ResizableBuffer = util.ResizableBuffer;
12+
const ResizableBuffer = types.ResizableBuffer;
1113

1214
const hasParseHeaderSupport = @import("util.zig").hasParseHeaderSupport;
1315

@@ -52,6 +54,7 @@ pub const FetchOptions = struct {
5254
method: Method = .GET,
5355
body: ?[]const u8 = null,
5456
headers: ?[]const [:0]const u8 = null,
57+
response_writer: ?AnyWriter = null,
5558
};
5659

5760
pub const Response = struct {
@@ -338,11 +341,11 @@ pub fn setUnixSocketPath(self: Self, path: [:0]const u8) !void {
338341
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_UNIX_SOCKET_PATH, path.ptr));
339342
}
340343

341-
pub fn setPrivate(self: Self, data: *anyopaque) !void {
344+
pub fn setPrivate(self: Self, data: *const anyopaque) !void {
342345
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_PRIVATE, data));
343346
}
344347

345-
pub fn setWritedata(self: Self, data: *anyopaque) !void {
348+
pub fn setWritedata(self: Self, data: *const anyopaque) !void {
346349
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEDATA, data));
347350
}
348351

@@ -360,19 +363,21 @@ pub fn setWritefunction(
360363
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEFUNCTION, func));
361364
}
362365

363-
/// Set `WRITEDATA` to context and `WRITEFUNCTION` to a function that calls with the context.
364-
pub fn setWriteContext(
366+
/// Set `WRITEDATA` to AnyWriter and `WRITEFUNCTION` to a function that calls with the writer.
367+
pub fn setAnyWriter(
365368
self: Self,
366-
context: anytype,
367-
comptime writeFunc: fn (@TypeOf(context), ptr: []const u8) usize,
369+
any_writer: *const AnyWriter,
368370
) !void {
369-
try self.setWritedata(context);
371+
try self.setWritedata(any_writer);
370372
try self.setWritefunction(struct {
371373
fn write(ptr: [*c]c_char, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint {
372374
const real_size = size * nmemb;
373-
const ctx: @TypeOf(context) = @alignCast(@ptrCast(user_data));
374375
const data = (@as([*]const u8, @ptrCast(ptr)))[0..real_size];
375-
return @intCast(writeFunc(ctx, data));
376+
const writer: *const AnyWriter = @alignCast(@ptrCast(user_data));
377+
const ret = writer.write(data) catch {
378+
return 0; // Indicate an error
379+
};
380+
return @intCast(ret);
376381
}
377382
}.write);
378383
}
@@ -426,25 +431,6 @@ pub fn perform(self: Self) !Response {
426431
};
427432
}
428433

429-
fn setWriteContextInner(self: Self, context: anytype) !void {
430-
const ctxType = @typeInfo(@TypeOf(context));
431-
switch (ctxType) {
432-
.void => {
433-
// No write context, do nothing.
434-
return;
435-
},
436-
.pointer => |ptr| {
437-
if (comptime !@hasDecl(ptr.child, "write")) {
438-
@compileError("writeContext must have a `write` function");
439-
}
440-
try self.setWriteContext(context, @field(ptr.child, "write"));
441-
},
442-
else => {
443-
@compileError("writeContext must be a pointer or void type, current: " ++ @typeName(@TypeOf(context)));
444-
},
445-
}
446-
}
447-
448434
/// Head issues a HEAD to the specified URL.
449435
pub fn head(self: Self, url: [:0]const u8) !Response {
450436
try self.setUrl(url);
@@ -463,7 +449,6 @@ pub fn fetch(
463449
self: Self,
464450
url: [:0]const u8,
465451
options: FetchOptions,
466-
writeContext: anytype,
467452
) !Response {
468453
try self.setUrl(url);
469454
try self.setMethod(options.method);
@@ -483,20 +468,22 @@ pub fn fetch(
483468
h.deinit();
484469
};
485470

486-
try self.setWriteContextInner(writeContext);
471+
if (options.response_writer) |writer| {
472+
try self.setAnyWriter(&writer);
473+
}
487474

488475
return try self.perform();
489476
}
490477

491478
/// Upload issues a PUT request to upload file.
492479
/// `writeContext` is the same as in `fetch`, used to write the response body.
493-
pub fn upload(self: Self, url: [:0]const u8, path: []const u8, writeContext: anytype) !Response {
480+
pub fn upload(self: Self, url: [:0]const u8, path: []const u8, any_writer: AnyWriter) !Response {
494481
var up = try Upload.init(path);
495482
defer up.deinit();
496483
try self.setUpload(&up);
497484

498485
try self.setUrl(url);
499-
try self.setWriteContextInner(writeContext);
486+
try self.setAnyWriter(&any_writer);
500487
return try self.perform();
501488
}
502489

src/root.zig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
const std = @import("std");
22
const util = @import("util.zig");
3+
const types = @import("types.zig");
34
pub const checkCode = @import("errors.zig").checkCode;
45

56
pub const Easy = @import("Easy.zig");
67
pub const Multi = @import("Multi.zig");
7-
pub const ResizableWriteContext = util.ResizableWriteContext;
8-
pub const FixedWriteContext = util.FixedWriteContext;
8+
pub const ResizableResponseWriter = types.ResizableResponseWriter;
9+
pub const FixedResponseWriter = types.FixedResponseWriter;
910

1011
pub const printLibcurlVersion = util.printLibcurlVersion;
1112
pub const hasParseHeaderSupport = util.hasParseHeaderSupport;

0 commit comments

Comments
 (0)