Skip to content

Commit

Permalink
polyfill struct_curl_header for old libcurl version
Browse files Browse the repository at this point in the history
  • Loading branch information
jiacai2050 committed Sep 17, 2023
1 parent 73ef123 commit 79799fe
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 85 deletions.
60 changes: 33 additions & 27 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
#+TITLE: Zig-curl
#+TITLE: zig-curl
#+DATE: 2023-09-16T23:16:15+0800
#+LASTMOD: 2023-09-17T00:39:46+0800
#+LASTMOD: 2023-09-17T12:15:26+0800
#+OPTIONS: toc:nil num:nil
#+STARTUP: content

[[https://github.com/jiacai2050/zig-curl/actions/workflows/CI.yml][https://github.com/jiacai2050/zig-curl/actions/workflows/CI.yml/badge.svg]]

[[https://curl.haxx.se/libcurl/][libcurl]] bindings for Zig.
Zig bindings to [[https://curl.haxx.se/libcurl/][libcurl]], a [[https://curl.se/docs/copyright.html][free]] and easy-to-use client-side URL transfer library.

* Usage
#+begin_src zig
const Easy = @import("curl").Easy;

pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const easy = try Easy.init(allocator);
defer easy.deinit();

const resp = try easy.get("http://httpbin.org/anything");
defer resp.deinit();

std.debug.print("Status code: {d}\nBody: {s}\n", .{
resp.status_code,
resp.body.items,
});
}
#+end_src
See [[file:examples/basic.zig][basic.zig]] for more usage.

* Installation
=zig-curl= support [[https://ziglang.org/download/0.11.0/release-notes.html#Package-Management][module]] introduced in Zig 0.11.

First add this package to =build.zig.zon= of your project like this:
#+begin_src zig
.{
Expand All @@ -27,35 +52,16 @@ const curl = b.dependency("curl", .{
.optimize = optimize,
});

exe.addModule("curl", sqlite.module("curl"));
exe.addModule("curl", curl.module("curl"));
// Note: since this package doesn't bundle static libcurl,
// so users need to link to system-wide libcurl.
exe.linkSystemLibrary("curl");
exe.linkLibC();
#+end_src

* Usage
#+begin_src zig
const Easy = @import("curl").Easy;

pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const easy = try Easy.init(allocator);
defer easy.deinit();

const resp = try easy.get("http://httpbin.org/anything");
defer resp.deinit();

std.debug.print("Status code: {d}\nBody: {s}\n", .{
resp.status_code,
resp.body.items,
});
}
#+end_src
More usage can refer to [[file:examples/basic.zig][basic.zig]].
* Roadmap
- [ ] Currently only easy API is supported, support [[https://curl.se/libcurl/c/libcurl-multi.html][multi API]].
- By default, this package will attempt to dynamically link to the system-wide libcurl and the system-wide SSL library.
- [ ] Support parse response header when [[https://curl.se/libcurl/c/curl_easy_header.html][libcurl < 7.84.0]]

* License
[[file:LICENSE][MIT]]
29 changes: 18 additions & 11 deletions examples/basic.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const std = @import("std");
const mem = std.mem;
const Allocator = mem.Allocator;
const Easy = @import("curl").Easy;
const curl = @import("curl");
const Easy = curl.Easy;

fn get(easy: Easy) !void {
const resp = try easy.get("http://httpbin.org/anything");
Expand All @@ -12,17 +13,17 @@ fn get(easy: Easy) !void {
resp.body.items,
});

// const date_header = try resp.get_header("date");
// if (date_header) |h| {
// std.debug.print("date header: {s}\n", .{h.get()});
// } else {
// std.debug.print("date header not found\n", .{});
// }
const date_header = try resp.get_header("date");
if (date_header) |h| {
std.debug.print("date header: {s}\n", .{h.get()});
} else {
std.debug.print("date header not found\n", .{});
}
}

fn post(easy: Easy) !void {
var payload = std.io.fixedBufferStream(
\\\ {"name": "John", "age": 15}
\\{"name": "John", "age": 15}
);
const resp = try easy.post("http://httpbin.org/anything", "application/json", payload.reader());
defer resp.deinit();
Expand All @@ -41,8 +42,14 @@ pub fn main() !void {
const easy = try Easy.init(allocator);
defer easy.deinit();

std.debug.print("-----------GET demo\n", .{});
try get(easy);
std.debug.print("-----------POST demo\n", .{});
curl.print_libcurl_version();

const sep = "-" ** 20;
std.debug.print("{s}GET demo{s}\n", .{ sep, sep });
get(easy) catch |e| {
std.debug.print("Get demo failed, error:{any}\n", .{e});
};

std.debug.print("{s}POST demo{s}\n", .{ sep, sep });
try post(easy);
}
67 changes: 67 additions & 0 deletions src/c.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("curl/curl.h");
});

pub fn print_libcurl_version() void {
const v = c.curl_version_info(c.CURLVERSION_NOW);
std.debug.print(
\\Libcurl build info
\\Host: {s}
\\Version: {s}
\\SSL version: {s}
\\Libz version: {s}
\\Protocols:
, .{
v.*.host,
v.*.version,
v.*.ssl_version,
v.*.libz_version,
});
var i: usize = 0;
while (v.*.protocols[i] != null) {
std.debug.print(" {s}", .{
v.*.protocols[i],
});
i += 1;
} else {
std.debug.print("\n", .{});
}

// feature_names is introduced in 7.87.0
if (@hasField(c.struct_curl_version_info_data, "feature_names")) {
std.debug.print("Features:", .{});
i = 0;
while (v.*.feature_names[i] != null) {
std.debug.print(" {s}", .{
v.*.feature_names[i],
});
i += 1;
} else {
std.debug.print("\n", .{});
}
}
}

pub fn polyfill_struct_curl_header() type {
if (has_curl_header()) {
return *c.struct_curl_header;
} else {
// return a dummy struct to make it compile on old version.
return struct {
value: [:0]const u8,
};
}
}

pub fn has_curl_header() bool {
// `curl_header` is officially supported since 7.84.0.
// https://curl.se/libcurl/c/curl_easy_header.html
return c.CURL_AT_LEAST_VERSION(7, 84, 0);
}

// comptime {
// if (!c.CURL_AT_LEAST_VERSION(7, 10, 0)) {
// @compileError("Must use libcurl >= 7.10.0");
// }
// }
93 changes: 46 additions & 47 deletions src/easy.zig
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
const c = @cImport({
@cInclude("curl/curl.h");
});
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const std = @import("std");
const mem = std.mem;
const fmt = std.fmt;

const testing = std.testing;
const has_curl_header = @import("c.zig").has_curl_header;
const polyfill_struct_curl_header = @import("c.zig").polyfill_struct_curl_header;

const Allocator = mem.Allocator;
const Self = @This();

allocator: mem.Allocator,
allocator: Allocator,
handle: *c.CURL,
/// The maximum time in milliseconds that the entire transfer operation to take.
timeout_ms: usize = 30_000,
Expand All @@ -31,9 +32,9 @@ pub const Method = enum {

pub const RequestHeader = struct {
entries: std.StringHashMap([]const u8),
allocator: mem.Allocator,
allocator: Allocator,

pub fn init(allocator: mem.Allocator) @This() {
pub fn init(allocator: Allocator) @This() {
return .{
.entries = std.StringHashMap([]const u8).init(allocator),
.allocator = allocator,
Expand All @@ -48,7 +49,7 @@ pub const RequestHeader = struct {
try self.entries.put(k, v);
}

/// Users should be free returned list (after usage) with `freeCHeader`.
// Note: Caller should free returned list (after usage) with `freeCHeader`.
fn asCHeader(self: @This()) !?*c.struct_curl_slist {
if (self.entries.count() == 0) {
return null;
Expand Down Expand Up @@ -94,11 +95,7 @@ pub fn Request(comptime ReaderType: type) type {
return if (self.verbose) 1 else 0;
}

fn getBody(self: @This(), allocator: mem.Allocator) !?[]u8 {
if (self.body == null) {
return null;
}

fn getBody(self: @This(), allocator: Allocator) !?[]u8 {
if (@TypeOf(self.body.?) == void) {
return null;
}
Expand All @@ -121,47 +118,49 @@ pub const Response = struct {
status_code: i32,

handle: *c.CURL,
allocator: mem.Allocator,
allocator: Allocator,

pub fn deinit(self: @This()) void {
self.body.deinit();
}

// Parse response header using `curl_easy_header` require libcurl >= 7.84.0.
// Find other solution to parse.
// pub const Header = struct {
// c_header: *c.struct_curl_header,
// name: []const u8,

// /// Get gets the first value associated with the given key.
// /// Applications need to copy the data if it wants to keep it around.
// pub fn get(self: @This()) []const u8 {
// return mem.sliceTo(self.c_header.value, 0);
// }
// };

// pub fn get_header(self: @This(), name: []const u8) !?Header {
// const c_name = try fmt.allocPrintZ(self.allocator, "{s}", .{name});
// defer self.allocator.free(c_name);

// var header: ?*c.struct_curl_header = null;
// // https://curl.se/libcurl/c/curl_easy_header.html
// const code = c.curl_easy_header(self.handle, name.ptr, 0, c.CURLH_HEADER, -1, &header);

// // https://curl.se/libcurl/c/libcurl-errors.html
// return switch (code) {
// c.CURLHE_OK => .{
// .c_header = header.?,
// .name = name,
// },
// c.CURLHE_MISSING => null,
// c.CURLHE_BADINDEX => error.BadIndex,
// else => error.HeaderOther,
// };
// }
pub const Header = struct {
c_header: polyfill_struct_curl_header(),
name: []const u8,

/// Get gets the first value associated with the given key.
/// Applications need to copy the data if it wants to keep it around.
pub fn get(self: @This()) []const u8 {
return mem.sliceTo(self.c_header.value, 0);
}
};

pub fn get_header(self: @This(), name: []const u8) errors.HeaderError!?Header {
if (comptime !has_curl_header()) {
return error.NoCurlHeaderSupport;
}

const c_name = try fmt.allocPrintZ(self.allocator, "{s}", .{name});
defer self.allocator.free(c_name);

var header: ?*c.struct_curl_header = null;
// https://curl.se/libcurl/c/curl_easy_header.html
const code = c.curl_easy_header(self.handle, name.ptr, 0, c.CURLH_HEADER, -1, &header);
return if (errors.headerErrorFrom(code)) |err| blk: {
break :blk switch (err) {
error.Missing => null,
else => err,
};
} else blk: {
break :blk .{
.c_header = header.?,
.name = name,
};
};
}
};

pub fn init(allocator: mem.Allocator) !Self {
pub fn init(allocator: Allocator) !Self {
const handle = c.curl_easy_init();
if (handle == null) {
return error.Init;
Expand Down
31 changes: 31 additions & 0 deletions src/errors.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const c = @import("c.zig").c;
const assert = @import("std").debug.assert;

pub const HeaderError = error{
BadIndex,
Missing,
NoHeaders,
NoRequest,
OutOfMemory,
BadArgument,
NotBuiltIn,

UnknownHeaderError,
/// This means there is no `curl_easy_header` method in current libcurl.
NoCurlHeaderSupport,
};

pub fn headerErrorFrom(code: c.CURLHcode) ?HeaderError {
// https://curl.se/libcurl/c/libcurl-errors.html
return switch (code) {
c.CURLHE_OK => null,
c.CURLHE_BADINDEX => error.BadIndex,
c.CURLHE_MISSING => error.Missing,
c.CURLHE_NOHEADERS => error.NoHeaders,
c.CURLHE_NOREQUEST => error.NoRequest,
c.CURLHE_OUT_OF_MEMORY => error.OutOfMemory,
c.CURLHE_BAD_ARGUMENT => error.BadArgument,
c.CURLHE_NOT_BUILT_IN => error.NotBuiltIn,
else => error.UnknownHeaderError,
};
}
2 changes: 2 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ pub const Easy = @import("easy.zig");
pub const request = Easy.request;
pub const Request = Easy.Request;
pub const Response = Easy.Response;

pub const print_libcurl_version = @import("c.zig").print_libcurl_version;

0 comments on commit 79799fe

Please sign in to comment.