├── .gitignore ├── README.md ├── build.zig ├── example └── basic │ └── main.zig └── src └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-curl 2 | 3 | cURL binding for Zig 4 | 5 | ## Usage 6 | 7 | ```zig 8 | var allocator = std.heap.page_allocator; 9 | var f = struct { 10 | fn f(data: []const u8) anyerror!usize { 11 | try std.io.getStdOut().writeAll(data); 12 | return data.len; 13 | } 14 | }.f; 15 | var res = try curl.get("https://google.com/", .{ .allocator = allocator, .cb = f }); 16 | if (res != 0) { 17 | var msg = try curl.strerrorAlloc(allocator, res); 18 | defer allocator.free(msg); 19 | std.log.warn("{s}", .{msg}); 20 | } 21 | ``` 22 | 23 | ## Requirements 24 | 25 | * libcurl 26 | 27 | ## Installation 28 | 29 | ``` 30 | $ zig build 31 | ``` 32 | 33 | ## Link to zig-curl 34 | 35 | add following function into your build.zig. 36 | 37 | ```zig 38 | fn linkToCurl(step: *std.build.LibExeObjStep) void { 39 | var libs = if (builtin.os.tag == .windows) [_][]const u8{ "c", "curl", "bcrypt", "crypto", "crypt32", "ws2_32", "wldap32", "ssl", "psl", "iconv", "idn2", "unistring", "z", "zstd", "nghttp2", "ssh2", "brotlienc", "brotlidec", "brotlicommon" } else [_][]const u8{ "c", "curl" }; 40 | for (libs) |i| { 41 | step.linkSystemLibrary(i); 42 | } 43 | if (builtin.os.tag == .linux) { 44 | step.linkSystemLibraryNeeded("libcurl"); 45 | } 46 | if (builtin.os.tag == .windows) { 47 | step.include_dirs.append(.{ .raw_path = "c:/msys64/mingw64/include" }) catch unreachable; 48 | step.lib_paths.append("c:/msys64/mingw64/lib") catch unreachable; 49 | } 50 | } 51 | ``` 52 | 53 | Then, call for the step. 54 | 55 | ```zig 56 | const exe = b.addExecutable("zig-curl-example", "src/main.zig"); 57 | exe.setTarget(target); 58 | exe.setBuildMode(mode); 59 | pkgs.addAllTo(exe); 60 | linkToCurl(exe); // DO THIS 61 | exe.install(); 62 | ``` 63 | 64 | ## License 65 | 66 | MIT 67 | 68 | ## Author 69 | 70 | Yasuhiro Matsumoto (a.k.a. mattn) 71 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | pub fn build(b: *std.build.Builder) void { 5 | // Standard release options allow the person running `zig build` to select 6 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 7 | const mode = b.standardReleaseOptions(); 8 | 9 | var curlPkg = std.build.Pkg{ 10 | .name = "curl", 11 | .source = std.build.FileSource{ .path = "./src/main.zig" }, 12 | }; 13 | 14 | const lib = b.addStaticLibrary("zig-curl", "src/main.zig"); 15 | lib.setBuildMode(mode); 16 | var libs = if (builtin.os.tag == .windows) [_][]const u8{ "c", "curl", "bcrypt", "crypto", "crypt32", "ws2_32", "wldap32", "ssl", "psl", "iconv", "idn2", "unistring", "z", "zstd", "nghttp2", "ssh2", "brotlienc", "brotlidec", "brotlicommon" } else [_][]const u8{ "c", "curl" }; 17 | for (libs) |i| { 18 | lib.linkSystemLibrary(i); 19 | } 20 | if (builtin.os.tag == .linux) { 21 | lib.linkSystemLibraryNeeded("libcurl"); 22 | } 23 | lib.install(); 24 | 25 | const main_tests = b.addTest("src/main.zig"); 26 | main_tests.setBuildMode(mode); 27 | if (builtin.os.tag == .windows) { 28 | main_tests.include_dirs.append(.{ .raw_path = "c:/msys64/mingw64/include" }) catch unreachable; 29 | main_tests.lib_paths.append("c:/msys64/mingw64/lib") catch unreachable; 30 | } 31 | main_tests.addPackage(curlPkg); 32 | main_tests.linkLibrary(lib); 33 | 34 | const exe = b.addExecutable("curl-basic", "example/basic/main.zig"); 35 | if (builtin.os.tag == .windows) { 36 | exe.include_dirs.append(.{ .raw_path = "c:/msys64/mingw64/include" }) catch unreachable; 37 | exe.lib_paths.append("c:/msys64/mingw64/lib") catch unreachable; 38 | } 39 | exe.setBuildMode(mode); 40 | exe.addPackage(curlPkg); 41 | exe.linkLibrary(lib); 42 | b.default_step.dependOn(&exe.step); 43 | exe.install(); 44 | 45 | const test_step = b.step("test", "Run library tests"); 46 | test_step.dependOn(&main_tests.step); 47 | } 48 | -------------------------------------------------------------------------------- /example/basic/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const curl = @import("curl"); 3 | 4 | pub fn main() anyerror!void { 5 | var allocator = std.heap.page_allocator; 6 | 7 | var cainfo = std.process.getEnvVarOwned(allocator, "CURL_CA_BUNDLE") catch try allocator.dupe(u8, ""); 8 | defer allocator.free(cainfo); 9 | 10 | var f = struct { 11 | fn f(data: []const u8) anyerror!usize { 12 | try std.io.getStdOut().writeAll(data); 13 | return data.len; 14 | } 15 | }.f; 16 | 17 | var req = curl.request{ 18 | .allocator = allocator, 19 | .cb = f, 20 | .sslVerify = true, 21 | .cainfo = cainfo, 22 | }; 23 | var res = try curl.get("https://google.com/", req); 24 | if (res != 0) { 25 | var msg = try curl.strerrorAlloc(allocator, res); 26 | defer allocator.free(msg); 27 | std.log.warn("{s}", .{msg}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const c = @cImport({ 3 | @cDefine("WIN32_LEAN_AND_MEAN", "1"); 4 | @cInclude("curl/curl.h"); 5 | @cInclude("string.h"); 6 | }); 7 | const testing = std.testing; 8 | 9 | const func = fn ([]const u8) anyerror!usize; 10 | 11 | pub const context = struct { 12 | resp: response, 13 | curl: ?*c.CURL, 14 | cb: ?func, 15 | }; 16 | 17 | pub const response = struct { 18 | allocator: std.mem.Allocator, 19 | status: c_long, 20 | headers: headers, 21 | 22 | const Self = @This(); 23 | 24 | pub fn init(allocator: std.mem.Allocator) Self { 25 | return Self{ 26 | .allocator = allocator, 27 | .status = 0, 28 | .headers = std.ArrayList(header).init(allocator), 29 | }; 30 | } 31 | 32 | pub fn deinit(self: *Self) void { 33 | for (self.headers.items) |h| { 34 | self.allocator.free(h.name); 35 | self.allocator.free(h.value); 36 | } 37 | self.headers.deinit(); 38 | } 39 | }; 40 | 41 | pub const request = struct { 42 | allocator: std.mem.Allocator, 43 | cb: ?func = null, 44 | headers: ?*headers = null, 45 | body: ?*[]const u8 = null, 46 | timeout: i32 = -1, 47 | sslVerify: bool = true, 48 | cainfo: ?[]const u8 = null, 49 | response: ?*response = null, 50 | }; 51 | 52 | pub const header = struct { 53 | name: []const u8, 54 | value: []const u8, 55 | }; 56 | 57 | pub const headers = std.ArrayList(header); 58 | 59 | fn headerFn(ptr: [*]const u8, size: usize, nmemb: usize, ctx: *context) usize { 60 | const data = ptr[0 .. size * nmemb]; 61 | if (ctx.resp.status == 0) { 62 | _ = c.curl_easy_getinfo(ctx.curl, c.CURLINFO_RESPONSE_CODE, &ctx.resp.status); 63 | } 64 | var i: usize = 0; 65 | while (i < data.len) : (i += 1) { 66 | if (data[i] == ':') { 67 | var h: header = undefined; 68 | h.name = ctx.resp.allocator.dupe(u8, data[0..i]) catch unreachable; 69 | while (i < data.len and std.ascii.isSpace(data[i + 1])) : (i += 1) {} 70 | h.value = ctx.resp.allocator.dupe(u8, data[i..]) catch unreachable; 71 | ctx.resp.headers.append(h) catch unreachable; 72 | break; 73 | } 74 | } 75 | return size * nmemb; 76 | } 77 | 78 | fn writeFn(ptr: [*]const u8, size: usize, nmemb: usize, ctx: *context) usize { 79 | const data = ptr[0 .. size * nmemb]; 80 | return ctx.cb.?(data) catch 0; 81 | } 82 | 83 | pub fn send(method: []const u8, url: []const u8, req: request) !u32 { 84 | var curl: ?*c.CURL = undefined; 85 | var res: c.CURLcode = undefined; 86 | curl = c.curl_easy_init(); 87 | if (curl == null) { 88 | return 0; 89 | } 90 | defer c.curl_easy_cleanup(curl); 91 | if (std.mem.eql(u8, method, "POST")) { 92 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_POST), @as(c_long, 1)); 93 | } else if (!std.mem.eql(u8, method, "GET")) { 94 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_CUSTOMREQUEST), @ptrCast([*]const u8, method)); 95 | } 96 | 97 | if (req.sslVerify) { 98 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_SSL_VERIFYPEER), @as(c_long, 1)); 99 | } 100 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_SSL_VERIFYPEER), @as(c_long, 1)); 101 | if (req.timeout >= 0) { 102 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_TIMEOUT), @as(c_long, req.timeout)); 103 | } 104 | if (req.cainfo != null and req.cainfo.?.len > 0) { 105 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_CAINFO), req.cainfo.?); 106 | } else { 107 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_CAINFO), @as(c_long, 0)); 108 | } 109 | 110 | var ctx: context = .{ 111 | .resp = .{ 112 | .allocator = req.allocator, 113 | .status = 0, 114 | .headers = headers.init(req.allocator), 115 | }, 116 | .curl = curl, 117 | .cb = req.cb, 118 | }; 119 | defer { 120 | if (req.response == null) { 121 | for (ctx.resp.headers.items) |h| { 122 | req.allocator.free(h.name); 123 | req.allocator.free(h.value); 124 | } 125 | ctx.resp.headers.deinit(); 126 | } else { 127 | req.response.?.* = ctx.resp; 128 | } 129 | } 130 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_URL), @ptrCast([*]const u8, url)); 131 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_FOLLOWLOCATION), @as(c_long, 1)); 132 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_HEADERFUNCTION), headerFn); 133 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_HEADERDATA), &ctx); 134 | if (req.cb != null) { 135 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_WRITEFUNCTION), writeFn); 136 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_WRITEDATA), &ctx); 137 | } 138 | 139 | if (req.headers != null) { 140 | var headerlist: *c.curl_slist = undefined; 141 | for (req.headers.?.items) |he| { 142 | var bytes = std.ArrayList(u8).init(req.allocator); 143 | defer bytes.deinit(); 144 | try bytes.writer().print("{s}: {s}", .{ he.name, he.value }); 145 | headerlist = c.curl_slist_append(headerlist, @ptrCast([*]const u8, bytes.items)); 146 | } 147 | _ = c.curl_easy_setopt(curl, c.CURLOPT_HTTPHEADER, headerlist); 148 | defer c.curl_slist_free_all(headerlist); 149 | } 150 | if (req.body != null) { 151 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_POST), @as(c_long, 1)); 152 | _ = c.curl_easy_setopt(curl, @bitCast(c_uint, c.CURLOPT_POSTFIELDS), @ptrCast([*]const u8, req.body)); 153 | } 154 | res = c.curl_easy_perform(curl); 155 | return @as(u32, res); 156 | } 157 | 158 | pub fn put(url: []const u8, req: request) !u32 { 159 | return send("PUT", url, req); 160 | } 161 | 162 | pub fn patch(url: []const u8, req: request) !u32 { 163 | return send("PATCH", url, req); 164 | } 165 | 166 | pub fn post(url: []const u8, req: request) !u32 { 167 | return send("POST", url, req); 168 | } 169 | 170 | pub fn delete(url: []const u8, req: request) !u32 { 171 | return send("DELETE", url, req); 172 | } 173 | 174 | pub fn get(url: []const u8, req: request) !u32 { 175 | return send("GET", url, req); 176 | } 177 | 178 | pub fn strerrorAlloc(allocator: std.mem.Allocator, res: u32) ![]const u8 { 179 | const msg = c.curl_easy_strerror(res); 180 | const len = c.strlen(msg); 181 | var mem = try allocator.alloc(u8, len + 1); 182 | var i: usize = 0; 183 | while (i <= len) : (i += 1) { 184 | mem[i] = msg[i]; 185 | } 186 | return mem; 187 | } 188 | 189 | test "basic test" { 190 | var allocator = std.testing.allocator; 191 | var f = struct { 192 | fn f(data: []const u8) anyerror!usize { 193 | try std.io.getStdOut().writeAll(data); 194 | return data.len; 195 | } 196 | }.f; 197 | 198 | var cainfo = try std.process.getEnvVarOwned(allocator, "CURL_CA_BUNDLE"); 199 | defer allocator.free(cainfo); 200 | 201 | var req = request{ 202 | .allocator = allocator, 203 | .sslVerify = true, 204 | .cainfo = cainfo, 205 | }; 206 | var res = try get("http://google.com/", req); 207 | try std.testing.expectEqual(@as(u32, 0), res); 208 | 209 | req = request{ 210 | .allocator = allocator, 211 | .cb = f, 212 | .sslVerify = true, 213 | .cainfo = cainfo, 214 | }; 215 | res = try get("http://google.com/", req); 216 | try std.testing.expectEqual(@as(u32, 0), res); 217 | 218 | req = request{ 219 | .allocator = allocator, 220 | .cb = f, 221 | .sslVerify = true, 222 | .cainfo = cainfo, 223 | .response = &response.init(allocator), 224 | }; 225 | defer req.response.?.deinit(); 226 | res = try get("http://google.com/", req); 227 | try std.testing.expectEqual(@as(u32, 0), res); 228 | } 229 | --------------------------------------------------------------------------------