Rminibwa alignment batches are native external pointers with borrowed column buffers. R users can inspect them through ALTREP vectors, but downstream packages should consume them through the installed C header:
The header resolves functions with R_GetCCallable(). A
downstream package therefore needs LinkingTo: Rminibwa for
the header and an ordinary R package runtime dependency to ensure
Rminibwa is loaded before the first call.
This vignette uses Rtinycc to compile the downstream
consumer in-process. The C source is displayed with Rtinycc’s C
rendering helper and compiled from the same
rminibwa_capi_code object.
library(Rminibwa)
td <- tempfile("rminibwa-capi-")
dir.create(td)
ref <- paste(rep("ACGT", 1000), collapse = "")
fa <- file.path(td, "ref.fa")
writeLines(c(">chr1", ref), fa, useBytes = TRUE)
prefix <- file.path(td, "idx")
mb_index_build(fa, prefix, threads = 1L)
idx <- mb_index_load(prefix)
aln <- charToRaw(substr(ref, 1L, 100L)) |>
mb_map(idx, opt = mb_opts("sr", out_n = 0L), name = charToRaw("read1"))
mb_align_n(aln)
#> [1] 51
mb_align_col(aln, "tid")[[1]]
#> [1] 0#define _Complex
#include <limits.h>
#include <stdint.h>
#include <stddef.h>
#include <R.h>
#include <Rinternals.h>
#include <Rminibwa.h>
static int clamp_size_to_int(size_t x)
{
return x > (size_t) INT_MAX ? INT_MAX : (int) x;
}
SEXP rminibwa_capi_summary(SEXP align_x)
{
const RmbAlignBatch *batch = Rminibwa_align_from_sexp(align_x);
const size_t n = Rminibwa_align_n(batch);
const size_t n_read = Rminibwa_align_n_read(batch);
const int32_t *read_len = Rminibwa_align_read_i32_col(batch, "length");
const int32_t *tid = Rminibwa_align_i32_col(batch, "tid");
const int32_t *qe = Rminibwa_align_i32_col(batch, "qe");
const int64_t *ts = Rminibwa_align_i64_col(batch, "ts");
size_t n_cigar_words = 0;
(void) Rminibwa_align_cigar_words(batch, &n_cigar_words);
SEXP out = PROTECT(Rf_allocVector(INTSXP, 5));
INTEGER(out)[0] = clamp_size_to_int(n);
INTEGER(out)[1] = clamp_size_to_int(n_read);
INTEGER(out)[2] = n_read && read_len ? read_len[0] : NA_INTEGER;
INTEGER(out)[3] = n && tid ? tid[0] : NA_INTEGER;
INTEGER(out)[4] = n && qe ? qe[0] : NA_INTEGER;
SEXP names = PROTECT(Rf_allocVector(STRSXP, 5));
SET_STRING_ELT(names, 0, Rf_mkChar("n"));
SET_STRING_ELT(names, 1, Rf_mkChar("n_read"));
SET_STRING_ELT(names, 2, Rf_mkChar("first_read_length"));
SET_STRING_ELT(names, 3, Rf_mkChar("first_tid"));
SET_STRING_ELT(names, 4, Rf_mkChar("first_qe"));
Rf_setAttrib(out, R_NamesSymbol, names);
/* Touch one 64-bit column so this example covers both integer-width paths. */
if (n_cigar_words == 0) INTEGER(out)[4] = NA_INTEGER;
if (n && ts && ts[0] < 0) INTEGER(out)[4] = NA_INTEGER;
UNPROTECT(2);
return out;
}ffi <- tryCatch(
Rtinycc::tcc_ffi() |>
Rtinycc::tcc_include(system.file("include", package = "Rminibwa")) |>
Rtinycc::tcc_source(rminibwa_capi_code) |>
Rtinycc::tcc_bind(
rminibwa_capi_summary = list(args = list("sexp"), returns = "sexp")
) |>
Rtinycc::tcc_compile(),
error = identity
)
if (inherits(ffi, "error")) {
cat("Rtinycc compilation is unavailable on this platform during vignette build:\n")
cat(conditionMessage(ffi), "\n")
} else {
ffi$rminibwa_capi_summary(aln)
}
#> n n_read first_read_length first_tid
#> 51 1 100 0
#> first_qe
#> 100The C function never asks R to materialize a data frame. It receives
the batch SEXP, obtains the opaque RmbAlignBatch *, and
reads borrowed int32_t, int64_t, and packed
CIGAR buffers directly.
For BAM preparation, use mb_query_stream() rather than
repeatedly converting alignment batches. It requires an explicit
mode and total threads budget, and returns
exactly one normalized QNAME group per native next call. In
paired mode it rejects mismatched names, mate ordering/count errors, and
malformed or truncated FASTQ before an ambiguous group is emitted. A
supplied @RG record is retained exactly; no sample or
library metadata is invented.
The installed API is deliberately a versioned POD/view interface
rather than a bam1_t handoff, so a consumer compiled
against its own htslib can encode each record once. Its header view
reports SO:unsorted and GO:query: contiguous
groups preserve input order but are not query-name sorted:
if (Rminibwa_stream_abi_version() != RMINIBWA_STREAM_ABI_VERSION)
Rf_error("Rminibwa stream ABI mismatch");
RmbQueryStream *stream = Rminibwa_stream_from_sexp(stream_x);
const RmbQueryGroup *group = NULL;
Rminibwa_header_view header;
Rminibwa_query_group_view_t view;
Rminibwa_stream_header(stream, &header); /* @SQ, exact @RG, @PG facts */
while (Rminibwa_stream_next(stream, &group) == RMINIBWA_STREAM_OK) {
Rminibwa_query_group_view(group, &view);
/* Consume view.reads and view.records before requesting the next group. */
/* record.cigar and typed AS/NM/MQ/MC/RG tags are native, not SAM text. */
}Every pointer in Rminibwa_query_group_view_t is borrowed
and is valid only until Rminibwa_stream_next(),
Rminibwa_stream_cancel(), or stream destruction. This
creates one-group back-pressure: a consumer pauses simply by not
requesting another group. Rminibwa_stream_cancel() releases
the current alignment buffers and leaves a reason code/message available
through the C API (or mb_query_stream_error() for
diagnostics). The producer has no extra worker pool;
threads is the complete minibwa mapping budget.