ABOUTME: Workspace overview for digests-core shared parsing library. ABOUTME: Documents crates, FFI, and platform usage patterns.
Rust workspace providing shared parsing primitives and a C ABI for multi-platform apps:
crates/feed: feed parsing (RSS/Atom/podcast) → DFeed ABI.crates/hermes: ReaderView/article extraction and metadata (Hermes port) → DReaderView / DMetadata.crates/ffi: C ABI surface over the parsers with arena-managed results.crates/cli: developer CLI for feed parsing.
cargo test -q # run all tests
cargo build -p digests-ffi --release # produce FFI library (libdigests_ffi.*)
## Documentation
Start with the documentation index and per-crate pages:
- `docs/index.md`
- `docs/overview.md`
- `docs/building.md`
- `docs/feed.md`
- `docs/hermes.md`
- `docs/ffi.md`
- `docs/cli.md`
- `docs/troubleshooting.md`Run the CLI from the workspace:
cargo run -p digests-cli -- https://example.com/feed.xmlBuild and run the binary:
cargo build -p digests-cli --release
./target/release/digests-cli https://example.com/feed.xmlCommon examples:
# Parse multiple feeds and emit an envelope
./target/release/digests-cli https://example.com/feed.xml https://example.com/podcast.xml
# Parse from stdin
cat feed.xml | ./target/release/digests-cli -
# Override the feed_url field when parsing a local file
./target/release/digests-cli --feed-url https://example.com/feed.xml ./local-copy.xmlUse --compact to emit compact JSON and --help for the full option list.
Functions (blocking):
digests_extract_reader(url_ptr, url_len, html_ptr, html_len, out_err) -> DReaderArena*digests_reader_result(arena) -> const DReaderView*digests_free_reader(arena)digests_extract_metadata(html_ptr, html_len, base_url_ptr, base_url_len, out_err) -> DMetaArena*digests_metadata_result(arena) -> const DMetadata*digests_free_metadata(arena)
All strings are UTF-8 slices (ptr+len, not null-terminated). Results live in an arena; free the arena when done. On success out_err->code == D_OK.
FFI calls are synchronous; wrap them off the main thread:
// Suppose you expose C functions via module map.
func extractReaderAsync(url: String, html: Data, completion: @escaping (DReaderView?, DError) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
var err = DError(code: D_OK, message: DString(data: nil, len: 0))
let arena = html.withUnsafeBytes { bytes in
url.withCString { cUrl in
digests_extract_reader(
UnsafeRawPointer(cUrl), url.utf8.count,
bytes.baseAddress, html.count,
&err
)
}
}
let view = arena.flatMap { digests_reader_result($0) }
DispatchQueue.main.async {
completion(view?.pointee, err)
if let arena = arena { digests_free_reader(arena) }
}
}
}fun extractReaderAsync(url: String, html: ByteArray, callback: (ReaderView?, Int) -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
val err = DError()
val arena = digests_extract_reader(url, html, err)
val view = arena?.let { digests_reader_result(it) }
withContext(Dispatchers.Main) {
callback(view, err.code)
arena?.let { digests_free_reader(it) }
}
}
}public static Task<ReaderView?> ExtractReaderAsync(string url, byte[] html) =>
Task.Run(() =>
{
var err = new DError();
var arena = digests_extract_reader(url, html, ref err);
var view = arena != IntPtr.Zero ? Marshal.PtrToStructure<ReaderView>(digests_reader_result(arena)) : (ReaderView?)null;
if (arena != IntPtr.Zero) digests_free_reader(arena);
return view;
});Adjust signatures to your actual FFI bindings; key point is to call the blocking C function on a background thread/queue/dispatcher, then marshal results back to the UI thread.
digests_extract_metadata is inexpensive; still treat it as blocking and wrap similarly if calling from UI threads.
- Strings are not null-terminated; always use
len. - Arenas own all returned memory; do not free individual pointers.
- Check
DIGESTS_FFI_VERSIONfor ABI compatibility in your bindings.