C Library API (experimental)¶
Warning
The C API is experimental. It is new in this release, and its functions, types and behaviour may change in later releases in ways that require callers to be updated.
SortMeRNA provides a reentrant C API for embedding rRNA filtering into other
applications without spawning a subprocess. The API is defined in
include/smr_api.h and links as a static library (libsmr_api.a).
The library never calls exit(), abort(), or assert(). All output
is routed through a caller-provided log callback; stdout and stderr are
suppressed during pipeline execution.
There are two ways to use it:
One-shot:
smr_run(reads from files) andsmr_run_seqs(reads from memory) build or load the index, align one set of reads, and return per-read results.Streaming:
smr_index_loadloads the index and references once (smr_index_load_seqsdoes the same with the reference sequences in memory);smr_run_seqs_with_indexthen aligns any number of in-memory batches against them;smr_index_freereleases them. This is for tools that align many batches against the same references (a database table function, a server handling requests, a FASTQ reader feeding chunks).
Quick start¶
#include "smr_api.h"
int main(void) {
/* 1. Configure */
smr_config_t cfg;
smr_config_init(&cfg);
cfg.num_threads = 4;
/* 2. Create context */
smr_context_t *ctx = smr_ctx_create(&cfg);
if (!ctx) return 1;
/* 3. Run alignment */
const char *refs[] = { "silva-bac-16s.fasta" };
const char *reads[] = { "sample.fastq" };
smr_output_t *out = NULL;
smr_stats_t stats;
int rc = smr_run(ctx, refs, 1, reads, 1, &out, &stats);
if (rc != SMR_OK) {
fprintf(stderr, "Error: %s\n", smr_last_error(ctx));
smr_ctx_destroy(ctx);
return 1;
}
printf("Total reads: %llu\n", (unsigned long long)stats.total_reads);
printf("Aligned: %llu\n", (unsigned long long)out->num_aligned);
/* 4. Inspect per-read results */
for (uint64_t i = 0; i < out->num_reads; i++) {
if (out->aligned[i])
printf("%s: %.1f%% id, e=%.2g, strand=%s, AS=%d, NM=%d\n",
out->read_ids[i], out->identity[i], out->e_value[i],
out->strand[i] ? "+" : "-",
out->score[i], out->edit_distance[i]);
}
/* 5. Clean up */
smr_output_free(out);
smr_ctx_destroy(ctx);
return 0;
}
In-memory input:
/* Pass sequences directly without writing temp files */
smr_seq_t seqs[] = {
{ "read1", "ACGTACGTACGT...", NULL },
{ "read2", "TGCATGCATGCA...", NULL },
};
int rc = smr_run_seqs(ctx, refs, 1, seqs, 2, &out, &stats);
Streaming:
cfg.workdir = "/data/smr_work"; /* keep the index between runs */
smr_context_t *ctx = smr_ctx_create(&cfg);
const char *refs[] = { "silva-bac-16s.fasta" };
smr_index_t *idx = smr_index_load(ctx, refs, 1);
if (!idx) { /* smr_last_error(ctx) */ }
for (int b = 0; b < num_batches; b++) {
smr_output_t *out = NULL;
smr_stats_t stats;
/* batch_seqs[b]: caller-owned smr_seq_t array of batch_len[b] reads */
int rc = smr_run_seqs_with_index(idx, batch_seqs[b], batch_len[b], &out, &stats);
if (rc != SMR_OK) { /* smr_last_error(ctx) */ continue; }
/* out->aligned[i], out->ref_name[i], out->e_value[i], ... */
smr_output_free(out);
}
smr_index_free(idx);
smr_ctx_destroy(ctx);
References from memory, for example the rows of a database table:
smr_seq_t refs[] = {
{ "ref1", "ACGTACGTACGT...", NULL },
{ "ref2", "TGCATGCATGCA...", NULL },
};
smr_index_t *idx = smr_index_load_seqs(ctx, refs, 2);
/* then smr_run_seqs_with_index and smr_index_free, as above */
Building¶
The library is built and installed together with the sortmerna binary, so
the build described in Building is all that is needed:
python3 setup.py sortmerna
This installs, under the install prefix (dist by default):
include/smr_api.h
lib/libsmr_api.a
and puts both into the generated .deb, .sh and .tar.gz packages
alongside bin/sortmerna.
The installed libsmr_api.a is self-contained: the API, the SortMeRNA core
objects and the static dependencies (RocksDB, Parasail, alp, zlib) are merged
into the one archive, so an application links nothing else but the system
libraries:
cc app.c -I <prefix>/include <prefix>/lib/libsmr_api.a \
-lstdc++ -lpthread -ldl -lm -o app
The archive is therefore around 55 MB. Note that it is produced by a separate
bundling step, so cmake --build build --target smr_api alone yields only
the unbundled build/src/smr_api/<config>/libsmr_api.a, which links solely
from within the build tree; build the smr_api_bundle target, or just
install, for the distributable one.
With -DWITH_TESTS=ON (set by the presets) ctest runs two tests for the
library: smr_api_smoke exercises the API against golden outputs, and
smr_api_bundle_link links a program against the bundled archive alone to
check that it really is self-contained.
API reference¶
Configuration¶
-
void smr_config_init(smr_config_t *cfg)¶
Initialize a configuration struct with sensible defaults. Must be called before modifying any fields. Sets
struct_size(uint32_t) for ABI version detection.After calling
smr_config_init, the struct is ready for immediate use without further modification.
-
type smr_config_t¶
Configuration struct. The first field (
struct_size) must always be set bysmr_config_init– never initialize this struct manually.Threading:
Field
Type
Default
num_threadsint32_t2
Alignment scoring:
evalueis the E-value threshold ofsmr_run;SMR_EVALUE_DEFAULTselects the sortmerna default (1e-5). The in-memory functions apply no threshold (see E-value semantics).Boolean flags (
int32_t, 0 = off, nonzero = on):Field
Default
best1 (on)
paired0
forward_only0
reverse_only0
full_search0
Paths:
Field
Type
Default
workdirconst char*NULL (auto temp dir)
When
workdiris NULL, eachsmr_runcall and each handle uses a new directory under the system temp directory (TMPDIR), removed afterwards. The index is then rebuilt every time.Reuse one
workdiracross calls to build the reference index only once. The index is written to<workdir>/idxand loaded from there by later calls with the same reference file names. As with the sortmerna CLI, the index is not rebuilt when a reference file’s contents orseed_win_lenchange; delete<workdir>/idxin that case.smr_index_load_seqskeeps its copies of the references in<workdir>/refs, named after their contents, so it does not have this problem. The library never deletesidx/orrefs/in a caller’s workdir; delete them to reclaim the space.<workdir>/kvdbis scratch space. The library deletes it before and after eachsmr_runand when a handle is loaded and freed, so results never carry over between calls. The sameworkdirmust not be used by two calls or handles at the same time.smr_runalso writes its reports (aligned.*,other.*) into the workdir.Logging:
When
log_callbackis NULL, the library operates silently. When set, all informational, warning, and error messages are routed through the callback. Thelog_user_datapointer is passed through unchanged.The callback also receives the messages of SortMeRNA’s worker threads, so it may run on a thread other than the caller’s. The library never runs it concurrently: calls are made one at a time. The callback must not call SortMeRNA functions.
Both
workdirandlog_user_dataare borrowed pointers – the pointee must outlive the context.
Context lifecycle¶
-
smr_context_t *smr_ctx_create(const smr_config_t *cfg)¶
Create a new context from a configuration struct. Returns an opaque pointer on success, or NULL on failure (invalid config or allocation failure).
The config is copied into the context – the caller may free or reuse the
smr_config_tstruct immediately after this call.- Parameters:
cfg – Pointer to an initialized config struct. Must not be NULL.
cfg->struct_sizemust equalsizeof(smr_config_t).
- Returns:
Opaque context pointer, or NULL on failure.
-
void smr_ctx_destroy(smr_context_t *ctx)¶
Destroy a context and free all associated resources. Passing NULL is safe (no-op). The context must not be used after this call.
Error reporting¶
-
const char *smr_strerror(int code)¶
Return a static string describing an error code category.
-
const char *smr_last_error(const smr_context_t *ctx)¶
Return a detailed, context-specific error message from the most recent failing call. Returns an empty string if no error has occurred or if
ctxis NULL.
-
int smr_last_error_code(const smr_context_t *ctx)¶
Return the numeric error code from the most recent failing call. Returns
SMR_OKif no error has occurred. ReturnsSMR_ERR_INVALID_CONFIGifctxis NULL.
Computation¶
-
int smr_run(smr_context_t *ctx, const char **ref_paths, int32_t num_refs, const char **read_paths, int32_t num_reads, smr_output_t **out, smr_stats_t *stats)¶
Run the SortMeRNA alignment pipeline, as the
sortmernabinary does.- Parameters:
ctx – Context created by
smr_ctx_create.ref_paths – Array of reference FASTA file paths.
num_refs – Number of reference files (must be > 0).
read_paths – Array of reads file paths (FASTA or FASTQ, plain or gzipped).
num_reads – Number of reads files (must be > 0).
out – Pointer to receive the output struct. May be NULL if only stats are needed. The caller must free the output with
smr_output_free.stats – Pointer to receive run statistics. May be NULL.
- Returns:
SMR_OKon success, or a negative error code on failure.
The function validates all inputs before running: NULL checks, file existence, and empty file detection. On error,
smr_last_error(ctx)provides a descriptive message.A context may be reused for multiple sequential
smr_runcalls. Each call produces an independent output that must be freed separately. E-values are those of the CLI and match itsaligned.blastreport.
-
int smr_run_seqs(smr_context_t *ctx, const char **ref_paths, int32_t num_refs, const smr_seq_t *seqs, int32_t num_seqs, smr_output_t **out, smr_stats_t *stats)¶
In-memory input. Accepts sequences directly instead of file paths; no read files are written. Equivalent to
smr_index_load, onesmr_run_seqs_with_indexcall andsmr_index_free, so the Streaming rules apply (one reference file, per-read e-values).- Parameters:
ctx – Context created by
smr_ctx_create.ref_paths – Array of reference FASTA file paths.
num_refs – Number of reference files (must be 1).
seqs – Array of
smr_seq_tinput sequences. For paired-end mode (cfg.paired = 1), sequences must be interleaved:[fwd0, rev0, fwd1, rev1, ...].num_seqsmust be even.num_seqs – Number of input sequences (must be > 0).
out – Pointer to receive the output struct. May be NULL.
stats – Pointer to receive run statistics. May be NULL.
- Returns:
SMR_OKon success, or a negative error code on failure.
-
type smr_seq_t¶
A single input sequence for the in-memory functions, or a reference sequence for
smr_index_load_seqs. All pointers are caller-owned and must remain valid for the duration of the call.Field
Type
Description
idconst char*Identifier (without
>or@)sequenceconst char*Nucleotide sequence
qualityconst char*Quality string, or NULL for FASTA
Within one call, either every sequence has a quality string or none has. Identifiers need not be unique.
-
type smr_output_t¶
Alignment results. Library-allocated; call
smr_output_freewhen done. The output is independent of the context and remains valid aftersmr_ctx_destroy.There is one entry per input read, in input order; paired input stays interleaved. Coordinates (
ref_start,ref_end) are 1-based, matching BLAST/SAM convention.Field
Type
Description
num_readsuint64_tTotal number of input reads
num_aligneduint64_tNumber of reads with at least one alignment
read_idsconst char**Read identifiers (first header token)
alignedint32_t*1 if aligned, 0 otherwise
ref_indexint32_t*Reference index, -1 if unaligned
e_valuedouble*E-value of best alignment
identitydouble*Percent identity (0–100)
coveragedouble*Query coverage (0–100)
ref_startint32_t*1-based start on reference
ref_endint32_t*1-based end on reference
cigarconst char**CIGAR string, NULL if unaligned
ref_nameconst char**Reference sequence ID, NULL if unaligned
strandint32_t*1 = forward, 0 = reverse-complement; -1 if unaligned
scoreint32_t*Smith-Waterman alignment score; -1 if unaligned
edit_distanceint32_t*Edit distance (mismatches + gaps); -1 if unaligned
-
type smr_stats_t¶
Summary statistics from a run. Value-only struct – no free required.
Field
Type
Description
total_readsuint64_tTotal reads in input
total_aligneduint64_tReads passing E-value threshold (any hit for the in-memory functions)
total_id_cov_passuint64_tReads passing both identity and coverage
total_denovouint64_tDe novo reads (failing ID and coverage)
min_read_lenuint32_tShortest read length
max_read_lenuint32_tLongest read length
wall_time_secdoubleWall clock time in seconds
-
void smr_output_free(smr_output_t *out)¶
Free an output struct and all library-owned memory within it. Passing NULL is safe (no-op).
Streaming¶
-
smr_index_t *smr_index_load(smr_context_t *ctx, const char **ref_paths, int32_t num_refs)¶
Load (building it first if needed) the index of one reference file and the reference sequences into memory. Returns NULL on failure;
smr_last_error(ctx)andsmr_last_error_code(ctx)give the reason.num_refsmust be 1: a handle holds one loaded index, and each reference file is a separate index. Concatenate several FASTA files into one, or pass all the sequences tosmr_index_load_seqs. An index that does not fit in a single part (about 320 Mbp of reference at the default index memory) is also refused withSMR_ERR_NOT_IMPLEMENTED.
-
smr_index_t *smr_index_load_seqs(smr_context_t *ctx, const smr_seq_t *refs, int32_t num_refs)¶
Like
smr_index_load, with the reference sequences given in memory. SortMeRNA builds and reads its index from a FASTA file, so the library writes the sequences to<workdir>/refs/refs_<hash>.fastaand indexes that file. The name is a hash of the sequences andseed_win_len.With a caller-supplied
workdir, the copy and its index are kept, and a later call with the same sequences reuses both instead of writing and indexing again. Different sequences, or a differentseed_win_len, get a copy and an index of their own. Without a workdir, both are removed bysmr_index_free.refs[i].idmust be non-empty and contain no whitespace; it is returned asref_name.refs[i].sequencemust be non-empty, contain no line breaks and not start with>.qualityis ignored. The pointers need only be valid during the call.Errors:
SMR_ERR_INVALID_CONFIGfor bad input,SMR_ERR_IOif the copy cannot be written,SMR_ERR_NOT_IMPLEMENTEDif the index needs more than one part, and those ofsmr_index_loadotherwise.
-
int smr_run_seqs_with_index(smr_index_t *idx, const smr_seq_t *seqs, int32_t num_seqs, smr_output_t **out, smr_stats_t *stats)¶
Align one in-memory batch against a loaded handle. Parameters and output are those of
smr_run_seqs. Errors and log messages go to the context the handle was loaded with. Batches on one handle may mix FASTA and FASTQ.
-
void smr_index_free(smr_index_t *idx)¶
Release a handle. Passing NULL is safe. The handle must not be used afterwards.
The handle keeps a pointer to its context, which must outlive it. Several
handles may exist at once, bound to the same or different contexts, but not
sharing one workdir. Loading a handle writes a small placeholder reads file
(placeholder_reads.fa) to the workdir to satisfy option parsing; it is
never read for sequence data.
E-value semantics¶
The in-memory functions report every positive Smith-Waterman hit and compute
its e-value with the per-query Karlin-Altschul form
E = K * m * n * exp(-lambda * S), where n is that read’s length and
m is the reference length stored in the index .stats file. The CLI,
and smr_run, differ in two ways:
n: the CLI uses the total length of all reads in the run. Library e-values are smaller by roughly the number of reads in a batch.
m: the CLI subtracts an edge-effect term that depends on the reads of the run. Library e-values are a further ~1-3% smaller for SILVA-scale references.
The CLI also drops hits below a minimum score derived from the e-value
threshold and the run’s reads; the library applies no such filter and
ignores cfg.evalue.
All three differences keep the per-read results independent of how reads
are grouped into batches: the same reads submitted as one batch or as many,
with any num_threads, give identical per-read output. Callers filtering
on e-value should calibrate against library output, not CLI output.
Version¶
-
const char *smr_version(void)¶
Return the SortMeRNA version string (e.g.,
"7.0.0").
Design notes¶
ABI stability¶
The smr_config_t struct uses struct_size (uint32_t) as its first
field for ABI version detection. uint32_t is used instead of size_t to
ensure consistent width across 32-bit and 64-bit platforms. smr_ctx_create
validates that the caller’s struct size matches the library’s, detecting
mismatches when a caller was compiled against a different header version.
All boolean fields use int32_t (not bool) for consistent sizing across
C and C++ compilers. Integer fields use explicit-width types from
<stdint.h>. The header requires only <stdint.h> (not <stddef.h>).
Opaque context¶
The smr_context_t type is an opaque pointer. Internal fields are hidden
from callers, allowing the library to change its internal layout without
breaking ABI. Contexts are created with smr_ctx_create and destroyed with
smr_ctx_destroy.
Error handling¶
The library uses a two-level error reporting scheme:
Error codes (return values): negative integers for errors, zero for success. Use
smr_strerrorfor category descriptions.Detailed messages (per-context):
smr_last_errorreturns a context-specific message with file paths, parameter values, or exception details from the most recent failing call.
The library never calls exit(), abort(), or assert(). Errors,
including those raised in worker threads, are propagated to the caller via
return codes.
I/O isolation¶
The library suppresses stdout/stderr output using two mechanisms:
Thread-local log routing: All internal logging macros (INFO, ERR, WARN) check a thread-local callback pointer that every API call sets for its duration, and that the worker threads it starts inherit. Messages go to the caller’s
log_callbackunder a process-wide lock, or are discarded when it is NULL. Thesortmernabinary sets no callback and prints to stdout/stderr.fd-level suppression: As a secondary defense against direct
std::cout/std::cerrwrites that bypass the macros,smr_runandsmr_run_seqs_with_indextemporarily redirect file descriptors 1 and 2 to/dev/nullviadup2. This is process-wide.
Log levels passed to the callback:
Constant |
Value |
|---|---|
|
0 |
|
1 |
|
2 |
|
3 |
Thread safety¶
smr_run and smr_run_seqs_with_index are serialized by a process-level
mutex. They are safe to call from multiple threads with independent contexts
or handles – calls will execute sequentially. smr_index_load and
smr_index_load_seqs hold the mutex while they parse options and, if
needed, build the index; loading the index and references into memory runs
concurrently with other calls. Context creation, destruction, and error
queries on different contexts are thread-safe and do not acquire the
mutex. Concurrent access to the same context from multiple threads is not
supported.
The mutex exists because stdout/stderr suppression uses dup2, which is
process-wide. The log callback routing itself uses thread-locals and is
lock-free.
Memory management¶
smr_ctx_createallocates the context withmalloc.smr_ctx_destroyfrees it withfree. Passing NULL is safe.smr_runand the in-memory functions allocate the output struct withcalloc.smr_output_freefrees the output and all library-owned arrays within it. Passing NULL is safe.The output is independent of the context and remains valid after
smr_ctx_destroy.Borrowed pointers in
smr_config_t(workdir,log_user_data) are not copied as strings – the pointee must outlive the context.