crossbind
GitHub

cURL for iOS

v8.22.0iOS

cURL 8.22.0 for React Native apps on iOS, precompiled for arm64 devices and simulators as @crossbind/port-curl-ios.

npm install @crossbind/port-curl-ios@beta

Install

shell
npm install @crossbind/plugin-react-native@beta @crossbind/plugin-react-native-ios-helper@beta @crossbind/port-curl-ios@beta
npm install --save-dev @crossbind/plugin-metro@beta
cd ios && pod install
crossbind.config.mjs
import curlIos from '@crossbind/port-curl-ios/crossbind.config.js';
 
export default {
dependencies: [curlIos],
paths: { config: import.meta.url },
};
metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const CrossbindMetroPlugin = require('@crossbind/plugin-metro');
 
const defaultConfig = getDefaultConfig(__dirname);
 
const config = {
...CrossbindMetroPlugin(defaultConfig),
};
 
module.exports = mergeConfig(defaultConfig, config);

The whole flow, including Expo, is in the React Native playbook.

Usage

The examples the WebAssembly page runs, as iOS compiles them: the same headers and the same calls. They are checked on the WebAssembly build.

Parse a URL the way curl will fetch it

libcurl's URL API: curl_url_set checks a URL and removes its dot segments, curl_url_get reads one part back, with CURLU_DEFAULT_PORT and CURLU_URLDECODE when asked, and curl_url_strerror explains a URL curl refuses. The host keeps the case it was written in.

src/native/url_parts.h
#pragma once
 
#include <curl/curl.h>
 
#include <stdexcept>
#include <string>
 
// libcurl's URL API, the parser curl runs on a URL before every transfer. Each call parses the URL
// into a fresh CURLU handle, reads one part back and frees the handle.
class UrlParts {
public:
// The URL as curl stores it: the scheme lowercased and dot segments removed. The host keeps its case.
static std::string normalize(const std::string& url) { return get(url, CURLUPART_URL, 0); }
 
static std::string host(const std::string& url) { return get(url, CURLUPART_HOST, 0); }
 
// CURLU_DEFAULT_PORT answers with the scheme's port when the URL does not name one.
static std::string port(const std::string& url) { return get(url, CURLUPART_PORT, CURLU_DEFAULT_PORT); }
 
static std::string path(const std::string& url) { return get(url, CURLUPART_PATH, 0); }
 
// CURLU_URLDECODE turns %XX sequences back into the bytes they stand for.
static std::string query(const std::string& url) { return get(url, CURLUPART_QUERY, CURLU_URLDECODE); }
 
private:
static std::string get(const std::string& url, CURLUPart part, unsigned int flags) {
CURLU* handle = curl_url();
if (!handle) throw std::runtime_error("out of memory");
char* value = nullptr;
CURLUcode code = curl_url_set(handle, CURLUPART_URL, url.c_str(), 0);
if (code == CURLUE_OK) code = curl_url_get(handle, part, &value, flags);
curl_url_cleanup(handle);
if (code != CURLUE_OK) throw std::runtime_error(curl_url_strerror(code));
const std::string result = value;
curl_free(value);
return result;
}
};
main.js
import { initNative, UrlParts } from './native/url_parts.h';
 
await initNative();
const url = 'HTTPS://Example.com/docs/../api/search?q=caf%C3%A9#results';
console.log(await UrlParts.normalize(url));
console.log(await UrlParts.host(url), await UrlParts.port(url), await UrlParts.path(url));
console.log(await UrlParts.query(url));
try {
await UrlParts.host('https://example.com:99999/');
} catch (error) {
console.log(error.message);
}
PRINTS
https://Example.com/api/search?q=caf%C3%A9#results
Example.com 443 /api/search
q=café
std::runtime_error: Port number was not a decimal number between 0 and 65535

Build a URL from parts without breaking it

curl_url_set with CURLU_APPENDQUERY and CURLU_URLENCODE adds a query pair with its value escaped, CURLU_URLENCODE on the path escapes a file name, and a relative reference set on a URL is resolved the way curl follows a redirect.

src/native/url_builder.h
#pragma once
 
#include <curl/curl.h>
 
#include <stdexcept>
#include <string>
 
// Builds URLs with libcurl's URL API instead of string concatenation: references are resolved the
// way curl follows a redirect, and CURLU_URLENCODE escapes what a path or a query cannot hold.
class UrlBuilder {
public:
// `reference` is resolved against `base`, as curl does with a Location header.
static std::string resolve(const std::string& base, const std::string& reference) { return edit(base, CURLUPART_URL, reference, 0); }
 
// Replaces the path, percent-encoding spaces and other characters a path cannot hold.
static std::string withPath(const std::string& url, const std::string& path) { return edit(url, CURLUPART_PATH, path, CURLU_URLENCODE); }
 
// Appends one name=value pair to the query. The value is encoded: a space becomes +, and a
// literal + or & becomes %2B or %26.
static std::string addQuery(const std::string& url, const std::string& pair) {
return edit(url, CURLUPART_QUERY, pair, CURLU_APPENDQUERY | CURLU_URLENCODE);
}
 
private:
static std::string edit(const std::string& url, CURLUPart part, const std::string& value, unsigned int flags) {
CURLU* handle = curl_url();
if (!handle) throw std::runtime_error("out of memory");
char* result = nullptr;
CURLUcode code = curl_url_set(handle, CURLUPART_URL, url.c_str(), 0);
if (code == CURLUE_OK) code = curl_url_set(handle, part, value.c_str(), flags);
if (code == CURLUE_OK) code = curl_url_get(handle, CURLUPART_URL, &result, 0);
curl_url_cleanup(handle);
if (code != CURLUE_OK) throw std::runtime_error(curl_url_strerror(code));
const std::string text = result;
curl_free(result);
return text;
}
};
main.js
import { initNative, UrlBuilder } from './native/url_builder.h';
 
await initNative();
let url = await UrlBuilder.resolve('https://api.example.com/v1/', 'search');
url = await UrlBuilder.addQuery(url, 'q=crème brûlée & tea');
url = await UrlBuilder.addQuery(url, 'sort=price+asc');
console.log(url);
console.log(await UrlBuilder.resolve(url, '../v2/items?id=7'));
console.log(await UrlBuilder.withPath(url, '/files/Q3 report.pdf'));
PRINTS
https://api.example.com/v1/search?q=cr%C3%A8me+br%C3%BBl%C3%A9e+%26+tea&sort=price%2Basc
https://api.example.com/v2/items?id=7
https://api.example.com/files/Q3%20report.pdf?q=cr%C3%A8me+br%C3%BBl%C3%A9e+%26+tea&sort=price%2Basc

Percent-encode and decode text

curl_easy_escape encodes every byte except letters, digits and - . _ ~, the encoding the curl tool's --data-urlencode and --url-query use. JavaScript's encodeURIComponent leaves ( ) ! * ' alone. curl_easy_unescape decodes %XX only: + stays +, and a % that starts no valid sequence is kept.

src/native/percent_codec.h
#pragma once
 
#include <curl/curl.h>
 
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <string>
 
// curl_easy_escape and curl_easy_unescape: percent-encoding the way curl does it. Every byte except
// A-Z, a-z, 0-9 and - . _ ~ is escaped, so the result is safe in any part of a URL. Since libcurl
// 7.82.0 both functions ignore their handle argument, so no easy handle is needed.
class PercentCodec {
public:
static std::string escape(const std::string& text) {
char* escaped = curl_easy_escape(nullptr, text.data(), static_cast<int>(text.size()));
return take(escaped, escaped ? std::strlen(escaped) : 0);
}
 
// Decodes %XX sequences only: + stays +, and a % that starts no valid sequence is kept.
static std::string unescape(const std::string& text) {
int size = 0;
char* decoded = curl_easy_unescape(nullptr, text.data(), static_cast<int>(text.size()), &size);
return take(decoded, static_cast<std::size_t>(size));
}
 
private:
static std::string take(char* data, std::size_t size) {
if (!data) throw std::runtime_error("out of memory");
const std::string result(data, size);
curl_free(data);
return result;
}
};
main.js
import { initNative, PercentCodec } from './native/percent_codec.h';
 
await initNative();
const text = 'crème brûlée & tea (50%)';
const escaped = await PercentCodec.escape(text);
console.log(escaped);
console.log(await PercentCodec.unescape(escaped));
console.log(encodeURIComponent(text));
console.log(await PercentCodec.unescape('a+b%20c%zz'));
PRINTS
cr%C3%A8me%20br%C3%BBl%C3%A9e%20%26%20tea%20%2850%25%29
crème brûlée & tea (50%)
cr%C3%A8me%20br%C3%BBl%C3%A9e%20%26%20tea%20(50%25)
a+b c%zz

Read the dates in HTTP headers

curl_getdate reads the three date formats HTTP allows, and variations such as zone names, as seconds since 1970 in UTC. A date without a zone is GMT, where JavaScript's Date.parse takes local time, and ISO 8601 is not a format it reads.

src/native/http_dates.h
#pragma once
 
#include <curl/curl.h>
 
#include <ctime>
#include <string>
 
// curl_getdate, the date parser curl uses for Last-Modified, Expires, cookie expiry and the tool's
// -z option. It reads the three HTTP date formats and many variations, and a date without a time
// zone is taken as GMT.
class HttpDates {
public:
// Seconds since 1970-01-01 UTC, or -1 when curl cannot read the text as a date.
static double seconds(const std::string& text) { return static_cast<double>(curl_getdate(text.c_str(), nullptr)); }
 
// The same instant written as ISO 8601 in UTC.
static std::string iso(const std::string& text) {
const time_t when = curl_getdate(text.c_str(), nullptr);
if (when == -1) return "not a date";
std::tm parts{};
gmtime_r(&when, &parts);
char buffer[32];
std::strftime(buffer, sizeof buffer, "%Y-%m-%dT%H:%M:%SZ", &parts);
return buffer;
}
};
main.js
import { initNative, HttpDates } from './native/http_dates.h';
 
await initNative();
const formats = ['Sun, 06 Nov 1994 08:49:37 GMT', 'Sunday, 06-Nov-94 08:49:37 GMT', 'Sun Nov 6 08:49:37 1994'];
for (const date of formats) {
console.log(await HttpDates.seconds(date), await HttpDates.iso(date));
}
console.log(await HttpDates.iso('Sun, 06 Nov 1994 08:49:37 CEST'));
console.log(await HttpDates.seconds('1994-11-06T08:49:37Z'));
PRINTS
784111777 1994-11-06T08:49:37Z
784111777 1994-11-06T08:49:37Z
784111777 1994-11-06T08:49:37Z
1994-11-06T06:49:37Z
-1

Check what this libcurl was built with

curl_version_info lists the protocols and features compiled in, so code can check before relying on one. This is the WebAssembly build: no HTTP/2, no compression, no IDN and no IPv6. Its protocols are compiled in, but in a browser a transfer goes through fetch, so only HTTP and HTTPS leave the page. The Android and iOS packages report the same protocols, plus libz.

src/native/build_info.h
#pragma once
 
#include <curl/curl.h>
 
#include <string>
 
// curl_version_info reports what this libcurl was built with, so code can check for a protocol or
// a feature before relying on it.
class BuildInfo {
public:
static std::string version() { return curl_version(); }
 
static std::string protocols() { return join(info()->protocols); }
 
static std::string features() { return join(info()->feature_names); }
 
static bool supports(const std::string& feature) {
for (const char* const* item = info()->feature_names; item && *item; ++item) {
if (feature == *item) return true;
}
return false;
}
 
private:
static const curl_version_info_data* info() { return curl_version_info(CURLVERSION_NOW); }
 
static std::string join(const char* const* items) {
std::string text;
for (; items && *items; ++items) text += (text.empty() ? "" : " ") + std::string(*items);
return text;
}
};
main.js
import { initNative, BuildInfo } from './native/build_info.h';
 
await initNative();
console.log(await BuildInfo.version());
console.log(await BuildInfo.protocols());
console.log(await BuildInfo.features());
console.log(await BuildInfo.supports('HTTP2'), await BuildInfo.supports('HSTS'));
PRINTS
libcurl/8.22.0 OpenSSL/4.0.2
dict file ftp ftps gopher gophers http https imap imaps mqtt mqtts pop3 pop3s rtsp smtp smtps telnet tftp ws wss
alt-svc AsynchDNS HSTS HTTPS-proxy Largefile SSL threadsafe UnixSockets
false true

What is different on iOS

  • pod install compiles your headers with the library through the plugin's podspec, and the app build links the result.
  • Named imports from ./native/<header>.h work as on the web: await initNative() once, then call the classes.
  • There is no m.FS and no /memfs: files live in the app's own storage, and your C++ takes their paths.
  • No Worker and no COOP or COEP: runtime: 'mt' uses pthreads directly.

Other platforms

Facts on this page come from the port manifests in the repository and from what npm served on beta when the site was built. See the Libraries guide for the full consumer flow.

MORE LIBRARIES
ExpatGDALGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close