crossbind
GitHub

Expat for Android

v2.8.5Android

Expat 2.8.5 for React Native apps on Android, precompiled for arm64-v8a devices and the x86_64 emulator as @crossbind/port-expat-android.

npm install @crossbind/port-expat-android@beta

Install

shell
npm install @crossbind/plugin-react-native@beta @crossbind/plugin-react-native-ios-helper@beta @crossbind/port-expat-android@beta
npm install --save-dev @crossbind/plugin-metro@beta
crossbind.config.mjs
import expatAndroid from '@crossbind/port-expat-android/crossbind.config.js';
 
export default {
dependencies: [expatAndroid],
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 Android compiles them: the same headers and the same calls. They are checked on the WebAssembly build.

Turn XML into JavaScript objects

The calls most Expat code makes: XML_ParserCreate, an element handler and a character data handler, then XML_Parse. A malformed document throws with the line and column where Expat stopped.

src/native/xml_tree.h
#pragma once
 
#include <expat.h>
 
#include <stdexcept>
#include <string>
#include <vector>
 
// XML in, JSON out. Each element becomes {"name","attributes","children","text"}: `children` holds
// its child elements and `text` its own character data, left out when it is only whitespace.
// Malformed XML throws Expat's message with the line and column where parsing stopped.
class XmlTree {
public:
static std::string version() {
const XML_Expat_Version v = XML_ExpatVersionInfo();
return std::to_string(v.major) + "." + std::to_string(v.minor) + "." + std::to_string(v.micro);
}
 
static std::string parse(const std::string& xml) {
Builder builder;
XML_Parser parser = XML_ParserCreate(nullptr);
if (!parser) throw std::runtime_error("out of memory");
XML_SetUserData(parser, &builder);
XML_SetElementHandler(parser, onStart, onEnd);
XML_SetCharacterDataHandler(parser, onText);
const bool ok = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), XML_TRUE) == XML_STATUS_OK;
const std::string error = ok ? "" : std::string(XML_ErrorString(XML_GetErrorCode(parser))) + " at line " +
std::to_string(XML_GetCurrentLineNumber(parser)) + ", column " +
std::to_string(XML_GetCurrentColumnNumber(parser));
XML_ParserFree(parser);
if (!ok) throw std::runtime_error(error);
return builder.json;
}
 
private:
struct Builder {
std::string json;
std::vector<std::string> text; // character data of each open element
std::vector<bool> hasChildren;
};
 
// Expat delivers UTF-8; JSON only needs quotes, backslashes and control characters escaped.
static std::string quote(const std::string& value) {
std::string out = "\"";
for (const char c : value) {
if (c == '"' || c == '\\') {
out += '\\';
out += c;
} else if (c == '\n') {
out += "\\n";
} else if (c == '\t') {
out += "\\t";
} else if (c == '\r') {
out += "\\r";
} else {
out += c;
}
}
return out + "\"";
}
 
static void XMLCALL onStart(void* data, const XML_Char* name, const XML_Char** attributes) {
Builder& builder = *static_cast<Builder*>(data);
if (!builder.hasChildren.empty()) {
if (builder.hasChildren.back()) builder.json += ",";
builder.hasChildren.back() = true;
}
builder.json += "{\"name\":" + quote(name) + ",\"attributes\":{";
for (int i = 0; attributes[i]; i += 2) builder.json += (i ? "," : "") + quote(attributes[i]) + ":" + quote(attributes[i + 1]);
builder.json += "},\"children\":[";
builder.text.emplace_back();
builder.hasChildren.push_back(false);
}
 
static void XMLCALL onText(void* data, const XML_Char* text, int length) {
static_cast<Builder*>(data)->text.back().append(text, static_cast<size_t>(length));
}
 
static void XMLCALL onEnd(void* data, const XML_Char*) {
Builder& builder = *static_cast<Builder*>(data);
const std::string& text = builder.text.back();
builder.json += "]";
if (text.find_first_not_of(" \t\r\n") != std::string::npos) builder.json += ",\"text\":" + quote(text);
builder.json += "}";
builder.text.pop_back();
builder.hasChildren.pop_back();
}
};
main.js
import { initNative, XmlTree } from './native/xml_tree.h';
 
await initNative();
console.log('Expat', await XmlTree.version());
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="1" lang="en"><title>Dune</title><price currency="EUR">9.99</price></book>
<book id="2" lang="fr"><title>L'Étranger</title><price currency="EUR">7.50</price></book>
<book id="3" lang="en"><title>Pride &amp; Prejudice</title><price currency="GBP">5.25</price></book>
</catalog>`;
const catalog = JSON.parse(await XmlTree.parse(xml));
for (const book of catalog.children) {
const [title, price] = book.children;
console.log(book.attributes.id, book.attributes.lang, title.text, price.text, price.attributes.currency);
}
 
try {
await XmlTree.parse('<catalog>\n <book id="4"><title>Emma</book>\n</catalog>');
} catch (error) {
console.log(error.cppMessage ?? error.message);
}
PRINTS
Expat 2.8.5
1 en Dune 9.99 EUR
2 fr L'Étranger 7.50 EUR
3 en Pride & Prejudice 5.25 GBP
mismatched tag at line 2, column 30

Read namespaced XML whatever the prefixes

XML_ParserCreateNS resolves every prefix to its namespace URI before your handlers see a name, and XML_SetStartNamespaceDeclHandler reports the declarations themselves.

src/native/xml_names.h
#pragma once
 
#include <expat.h>
 
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
 
// Namespace-aware parsing. XML_ParserCreateNS hands every name over as "<namespace URI>|<local name>",
// so what a document calls its prefixes no longer matters; names outside any namespace stay bare.
class XmlNames {
public:
// The text inside every element named `local` in the namespace `uri`, as a JSON array of strings.
static std::string textOf(const std::string& xml, const std::string& uri, const std::string& local) {
Search search;
search.name = uri.empty() ? local : uri + "|" + local;
Parser parser = create(&search);
XML_SetElementHandler(parser.get(), onStart, onEnd);
XML_SetCharacterDataHandler(parser.get(), onText);
parse(parser.get(), xml);
std::string json = "[";
for (const std::string& text : search.found) json += (json.size() > 1 ? "," : "") + quote(text);
return json + "]";
}
 
// Every prefix the document declares, with its URI, as JSON; "" is the default namespace.
static std::string declarations(const std::string& xml) {
std::map<std::string, std::string> declared;
Parser parser = create(&declared);
XML_SetStartNamespaceDeclHandler(parser.get(), [](void* data, const XML_Char* prefix, const XML_Char* uri) {
(*static_cast<std::map<std::string, std::string>*>(data))[prefix ? prefix : ""] = uri ? uri : "";
});
parse(parser.get(), xml);
std::string json = "{";
for (const auto& [prefix, uri] : declared) json += (json.size() > 1 ? "," : "") + quote(prefix) + ":" + quote(uri);
return json + "}";
}
 
private:
using Parser = std::unique_ptr<XML_ParserStruct, void (*)(XML_Parser)>;
 
struct Search {
std::string name;
std::vector<std::string> open; // the text of each open element with that name
std::vector<std::string> found;
};
 
static Parser create(void* data) {
Parser parser(XML_ParserCreateNS(nullptr, '|'), XML_ParserFree);
if (!parser) throw std::runtime_error("out of memory");
XML_SetUserData(parser.get(), data);
return parser;
}
 
static void parse(XML_Parser parser, const std::string& xml) {
if (XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), XML_TRUE) != XML_STATUS_OK) {
throw std::runtime_error(std::string(XML_ErrorString(XML_GetErrorCode(parser))) + " at line " +
std::to_string(XML_GetCurrentLineNumber(parser)) + ", column " +
std::to_string(XML_GetCurrentColumnNumber(parser)));
}
}
 
static void XMLCALL onStart(void* data, const XML_Char* name, const XML_Char**) {
Search& search = *static_cast<Search*>(data);
if (search.name == name) search.open.emplace_back();
}
 
static void XMLCALL onText(void* data, const XML_Char* text, int length) {
Search& search = *static_cast<Search*>(data);
if (!search.open.empty()) search.open.back().append(text, static_cast<size_t>(length));
}
 
static void XMLCALL onEnd(void* data, const XML_Char* name) {
Search& search = *static_cast<Search*>(data);
if (search.name != name) return;
search.found.push_back(search.open.back());
search.open.pop_back();
}
 
static std::string quote(const std::string& value) {
std::string out = "\"";
for (const char c : value) {
if (c == '"' || c == '\\') {
out += '\\';
out += c;
} else if (c == '\n') {
out += "\\n";
} else if (c == '\t') {
out += "\\t";
} else if (c == '\r') {
out += "\\r";
} else {
out += c;
}
}
return out + "\"";
}
};
main.js
import { initNative, XmlNames } from './native/xml_names.h';
 
await initNative();
const GPX = 'http://www.topografix.com/GPX/1/1';
const HR = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
// The same heart rates, written by two exporters that picked different prefixes for Garmin's extension.
const track = (prefix) => `<gpx version="1.1" creator="example" xmlns="${GPX}" xmlns:${prefix}="${HR}">
<trk><trkseg>
<trkpt lat="46.5190" lon="6.5668"><extensions><${prefix}:TrackPointExtension><${prefix}:hr>128</${prefix}:hr></${prefix}:TrackPointExtension></extensions></trkpt>
<trkpt lat="46.5192" lon="6.5671"><extensions><${prefix}:TrackPointExtension><${prefix}:hr>131</${prefix}:hr></${prefix}:TrackPointExtension></extensions></trkpt>
</trkseg></trk>
</gpx>`;
for (const prefix of ['gpxtpx', 'ns3']) {
const rates = JSON.parse(await XmlNames.textOf(track(prefix), HR, 'hr'));
console.log(`${prefix}:hr`, rates.join(' '));
}
console.log('hr in the GPX namespace:', JSON.parse(await XmlNames.textOf(track('gpxtpx'), GPX, 'hr')).length);
console.log(await XmlNames.declarations(track('ns3')));
PRINTS
gpxtpx:hr 128 131
ns3:hr 128 131
hr in the GPX namespace: 0
{"":"http://www.topografix.com/GPX/1/1","ns3":"http://www.garmin.com/xmlschemas/TrackPointExtension/v1"}

Expand entities without a billion laughs

Expat expands the entities a document declares and stops the ones that blow up: XML_SetBillionLaughsAttackProtectionMaximumAmplification and …ActivationThreshold set how far.

src/native/xml_guard.h
#pragma once
 
// expat.h declares the amplification limits only when XML_GE is 1; this build compiles them in.
#ifndef XML_GE
#define XML_GE 1
#endif
#include <expat.h>
 
#include <memory>
#include <stdexcept>
#include <string>
 
// Untrusted XML with entities expanded under Expat's limits. Once `activationBytes` of input and
// entity text have been processed, the expansion may be at most `maxAmplification` times the input;
// Expat's defaults are 100 and 8 MiB. A breach stops the parse like any other error.
class XmlGuard {
public:
// The document's character data, entities expanded.
static std::string text(const std::string& xml, double maxAmplification, double activationBytes) {
std::unique_ptr<XML_ParserStruct, void (*)(XML_Parser)> parser(XML_ParserCreate(nullptr), XML_ParserFree);
if (!parser) throw std::runtime_error("out of memory");
if (!XML_SetBillionLaughsAttackProtectionMaximumAmplification(parser.get(), static_cast<float>(maxAmplification))) {
throw std::invalid_argument("the maximum amplification must be at least 1");
}
if (activationBytes < 0 || !XML_SetBillionLaughsAttackProtectionActivationThreshold(parser.get(), static_cast<unsigned long long>(activationBytes))) {
throw std::invalid_argument("the activation threshold must be a byte count");
}
std::string text;
XML_SetUserData(parser.get(), &text);
XML_SetCharacterDataHandler(parser.get(), [](void* data, const XML_Char* chunk, int length) {
static_cast<std::string*>(data)->append(chunk, static_cast<size_t>(length));
});
if (XML_Parse(parser.get(), xml.data(), static_cast<int>(xml.size()), XML_TRUE) != XML_STATUS_OK) {
throw std::runtime_error(std::string(XML_ErrorString(XML_GetErrorCode(parser.get()))) + " at line " +
std::to_string(XML_GetCurrentLineNumber(parser.get())) + ", column " +
std::to_string(XML_GetCurrentColumnNumber(parser.get())));
}
return text;
}
};
main.js
import { initNative, XmlGuard } from './native/xml_guard.h';
 
await initNative();
const MAX_AMPLIFICATION = 100; // Expat's defaults
const THRESHOLD = 8 * 1024 * 1024;
console.log(await XmlGuard.text('<!DOCTYPE note [<!ENTITY product "crossbind">]><note>&product; parses &product;</note>', MAX_AMPLIFICATION, THRESHOLD));
 
// Each level repeats the one below ten times: 9 levels would expand to 3 GB.
const laughs = (levels) => {
const entities = ['<!ENTITY lol0 "lol">'];
for (let level = 1; level <= levels; level += 1) entities.push(`<!ENTITY lol${level} "${`&lol${level - 1};`.repeat(10)}">`);
return `<?xml version="1.0"?>\n<!DOCTYPE lolz [\n${entities.join('\n')}\n]>\n<lolz>&lol${levels};</lolz>`;
};
console.log('5 levels:', (await XmlGuard.text(laughs(5), MAX_AMPLIFICATION, THRESHOLD)).length, 'characters');
try {
await XmlGuard.text(laughs(9), MAX_AMPLIFICATION, THRESHOLD);
} catch (error) {
console.log('9 levels:', error.cppMessage ?? error.message);
}
try {
await XmlGuard.text(laughs(5), MAX_AMPLIFICATION, 64 * 1024);
} catch (error) {
console.log('5 levels, 64 KiB threshold:', error.cppMessage ?? error.message);
}
PRINTS
crossbind parses crossbind
5 levels: 300000 characters
9 levels: limit on input amplification factor (from DTD and entities) breached at line 14, column 6
5 levels, 64 KiB threshold: limit on input amplification factor (from DTD and entities) breached at line 10, column 6

"Stream a file through Expat" writes its input with m.FS, which Android does not have; the C++ takes paths, so it works unchanged on files in the app's storage. It runs on the WebAssembly page.

What is different on Android

  • The React Native plugin compiles your headers with the library inside Gradle's native build, so npm run android builds everything.
  • 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
cURLGDALGEOSGeoTIFFiconvLERClibjpeg-turbolibTIFFOpenSSLPROJSpatiaLiteSQLiteWebPzlibZstandard
Type to search every guide page and section.
↑↓ navigate↵ openesc close