C API
Exodus series and pipeline Version 2
mio_exodus_series_create(path) returns an owning handle or NULL; mio_exodus_series_write_points_cells(series, mesh) fixes geometry, node/side sets and attributes, then mio_exodus_series_write_data(series, time, mesh) appends a stable point/cell field schema. Flush/finalize return status and are idempotent; num_steps returns -1 on error, finalized returns 0/1 or -1, and free is NULL-safe. No option layout changed. Without netCDF the constructor names the missing dependency through mio_last_error. See Exodus.
The existing mio_pipeline_run_* and report counterparts accept Version 2 spatial documents: per-step Inputs and terminal Output.Pattern. Their signatures and ownership rules are unchanged. Sequence entry points still require transient Version 1 documents; spatial branches and time fan-out are not implicitly combined.
Pipeline reports
Pipeline reports are available through new owning mio_pipeline_run_*_report and mio_sequence_pipeline_run_*_report handles. Read JSON with caller-buffer mio_pipeline_report_json (length excludes NUL; -1 on error), then free with mio_pipeline_report_free. Size queries and repeated reads do not rerun the pipeline. Existing status-only functions remain unchanged. See structured pipeline reports.
MED named meshes
With HDF5 enabled, mio_med_mesh_count(path) and mio_med_mesh_name(path, index, buf, buflen) enumerate meshes in file link order without materializing geometry (zero-based index; string accessor rule 5). mio_med_read_named(path, name, opts) returns an owning mio_mesh*, freed with mio_mesh_free. mio_med_write_multi(path, meshes, names, count, version) borrows mesh/name arrays for the call; names must be unique/nonempty with no / or @, and NULL version selects 4.1.0. Named nodal/element profiles expand with NaN fill. Count/name return -1, reads return NULL and writes return an error status on failure; mio_last_error() explains it, including builds without HDF5. Existing option structs are unchanged. See MED.
The existing XDMF series mio_xdmf_series_write_points_cells call persists the mesh's fixed point/cell/side regions once with the shared topology. All steps, including raw-array and appended steps, reuse them; no ABI additions are needed. See shared named regions.
Native serial VTK XML reads (vtu, vtp, vts, vtr, vti) support multiple pieces without welding, appended raw/base64 arrays, UInt32/UInt64 headers and either byte order. VTP/VTS/VTR/VTI write inline arrays; codec availability follows the configured native build. Legacy vtk also reads structured points, structured grids and rectilinear grids in ASCII and big-endian binary. No new ABI options are required. See formats.
The C++ core also ships as an installable shared library, libmeshioplusplus, with a stable pure-C99 header — the natural entry point for HPC codes written in C (and the foundation of the Fortran interface). Like the WebAssembly binding, it is a flat, whole-mesh API over the same C++ core, built exclusively on the uniform mesh API, so it works identically under every mesh backend.
Building and installing
The C API is off by default (a plain pip install never builds it). From the repo:
build/configure.sh --c-api --build # add --fortran for the Fortran module
cmake --install build/cpp-release --prefix /opt/meshioplusplus(or pass -DMESHIOPLUSPLUS_BUILD_C_API=ON to a direct CMake configure). The install lays out:
include/meshioplusplus/meshioplusplus.h # the only installed header
lib/libmeshioplusplus.so[.0.6.1.0]
lib/cmake/meshioplusplus/ # find_package(meshioplusplus)
lib/pkgconfig/meshioplusplus.pc # pkg-configConsume it with pkg-config:
gcc my_solver.c $(pkg-config --cflags --libs meshioplusplus) -o my_solveror CMake:
find_package(meshioplusplus REQUIRED) # pin a minimum version if you need one, e.g. `meshioplusplus 16.0`
# (compatibility is SameMajorVersion: a 15.x request rejects a 16.x install)
target_link_libraries(my_solver PRIVATE meshioplusplus::meshioplusplus)HDF5/netCDF/zlib are detected at configure time exactly as for the Python build; they are private dependencies of the shared library (consumers never link them directly).
Package managers (Conan, vcpkg & Spack)
The C API also ships as a Conan recipe (root conanfile.py) and a vcpkg overlay port (packages/vcpkg/meshioplusplus/). Both are self-hosted in the repo and drive the same -DMESHIOPLUSPLUS_BUILD_C_API=ON -DMESHIOPLUSPLUS_BUILD_PYTHON=OFF install path, so consumers get the identical meshioplusplus::meshioplusplus target:
# Conan (options: with_hdf5 / with_netcdf / with_zlib / with_eigen / fortran)
conan create . -o meshioplusplus/*:with_hdf5=True -o meshioplusplus/*:with_netcdf=True
# vcpkg (features: hdf5 / netcdf / zlib -- all on by default)
vcpkg install meshioplusplus --overlay-ports=packages/vcpkgpackages/vcpkg/ also carries a second overlay, packages/vcpkg/libaec/, alongside packages/vcpkg/meshioplusplus/ — it replaces vcpkg registry's own libaec port (a transitive dependency of hdf5's szip feature) with an otherwise-identical one that sources the same release from libaec's official GitHub mirror instead of its gitlab.dkrz.de host, which had been intermittently rate-limiting this project's release CI. --overlay-ports=packages/vcpkg picks up both automatically.
Both are validated in CI on every PR and on v* release tags (.github/workflows/packages.yml). Two caveats: the shared library is shared-only (no static build yet), and the vendored Eigen submodule is off in both recipes (the MED transpose falls back to a hand-written loop), since it is absent from a source tarball. Neither is submitted to Conan Center / the upstream vcpkg registry today, so nothing resolves meshioplusplus as a plain requirement out of the box; as a stopgap, every v* release attaches ready-to-use Linux artifacts, for both x86_64 and arm64, to its GitHub Release that supply the missing recipe:
# Conan: restore the release archive (adds the meshioplusplus recipe + a matching
# prebuilt binary to your local cache), then install normally -- Conan Center
# still supplies the transitive hdf5/netcdf/zlib/openssl binaries over the
# network as usual; --build=missing rebuilds meshioplusplus itself from the
# archive's recipe if your profile doesn't match the prebuilt one.
conan cache restore meshioplusplus-conan-full-linux-x86_64.tgz # or ...-linux-arm64.tgz
conan install --requires=meshioplusplus/<version> --build=missing \
-o meshioplusplus/*:with_hdf5=True -o meshioplusplus/*:with_netcdf=True -o meshioplusplus/*:with_zlib=True
# vcpkg, from the release archive (unzip first):
vcpkg install meshioplusplus --overlay-ports=meshioplusplus-vcpkg-overlay-port-<version>Unlike Conan/vcpkg, the Spack recipe is not self-hosted in this repo — it lives in the spack/spack-packages upstream repo (meshioplusplus, PR #5624), so it needs no local checkout and resolves meshioplusplus as a plain requirement:
spack install meshioplusplus +fortran +hdf5 +netcdf +zlibVariants mirror the Conan options / vcpkg features above (+hdf5/+netcdf/+zlib/+zstd/+lz4/+bzip2/+kahip/+cgnslib/+adios2, all named the same way) plus +fortran, +cxx_api (the installable C++ API, with cxx_api_backends=meshio,native,kratos selecting which mesh-backend libraries to install), parallel=auto|seq|stl|openmp|tbb|kokkos, and mesh_backend=meshio|native|kratos for the standalone build. Two Conan/vcpkg options are deliberately not Spack variants: with_tecio, because Spack's tecio is manual_download and the .szplt reader needs a Tecplot install, and with_gidpost, because gidpost is vendored in-tree and hard-gated on zlib rather than on a Spack package, so it follows +zlib. with_polyscope, with_eigen and with_json are likewise not variants: the first two are vendored git submodules the release tarball omits, and the third degrades to a no-op when its vendored copy is absent. See Installation → Spack for the matching py-meshioplusplus Python package.
Example
The complete, CI-tested example lives at doc/examples/c_api_example.c:
#include <meshioplusplus/meshioplusplus.h>
/* Build a mesh from raw arrays and write it (setters copy your buffers). */
mio_mesh* m = mio_mesh_create();
mio_mesh_set_points(m, MIO_FLOAT64, num_points, 3, xyz); /* (n, 3) row-major */
mio_mesh_add_cell_block(m, "tetra", num_cells, 4, MIO_INT64, conn); /* 0-based indices */
mio_mesh_add_point_data(m, "temperature", MIO_FLOAT64, 1, (int64_t[]){num_points}, temp);
if (mio_write("out.vtu", m, NULL) != MIO_OK) /* NULL: infer format from extension */
fprintf(stderr, "%s\n", mio_last_error());
mio_mesh_free(m);
/* Read one back (getters are zero-copy borrows into mesh-owned memory). */
mio_mesh* r = mio_read("in.msh", NULL); /* .msh defaults to gmsh */
const void* pts; mio_dtype dt;
mio_mesh_get_points(r, &pts, &dt); /* valid until mutation/free */
mio_mesh_free(r);
/* Or convert file-to-file without touching the data. */
mio_convert("in.msh", NULL, "out.vtk", NULL);The contract in five rules
- Errors: every fallible function returns a
mio_status(MIO_OK == 0) orNULL/-1; the message is retrievable viamio_last_error()(thread-local, valid until the nextmio_*call on the same thread). No C++ exception ever crosses the ABI. - Setters copy — your buffers can be freed as soon as the call returns.
- Getters are zero-copy — returned data pointers alias mesh-owned memory and stay valid until the next mutating call on that mesh or
mio_mesh_free(). Read-only accessors never invalidate them. - Arrays are row-major (C order): points
(num_points, dim), connectivity(num_cells, nodes_per_cell)with 0-based node indices. - String getters use the snprintf convention: copy at most
buflen - 1bytes plus a NUL, return the full length (excluding the NUL),-1on error.
API reference
| Group | Functions |
|---|---|
| Introspection | mio_version, mio_mesh_backend, mio_format_readable, mio_format_writable, mio_last_error |
| Cell-type metadata | mio_cell_type_name, mio_cell_type_from_name, mio_cell_type_num_nodes, mio_cell_type_dimension (the mio_cell_type enum mirrors the C++ table; strings like "tetra10" are the primary representation; the seven MIO_CELL_VtkBezier* values were appended in v16.40.0, which moved MIO_CELL_Custom from 77 to 84, so code compiled against an older header must be rebuilt) |
| Lifecycle & I/O | mio_mesh_create, mio_mesh_free, mio_read, mio_write, mio_convert (format NULL/"" = infer from extension; .msh → gmsh, .inp → abaqus, .node/.ele → tetgen — pass "triangle" for 2D Triangle pairs; .mesh → medit, unless the file's first line names an MFEM mesh, which goes to mfem; Z88's fixed file names z88i1.txt, z88structure.txt, z88o2.txt, z88o3.txt → z88 rather than xyz; .dat → tecplot, unless the file opens as an MSC Marc input deck, which goes to marc; .plt → tecplot (binary, read-only); a file named d3plot → lsdyna_d3plot; a file named like crashA001 (a stem, A and three or more digits, no extension) → radioss_anim; a file named binout (or binout0000…) → lsdyna_binout; a file named like crashT01 (a stem, T and two digits, no extension) → radioss_th — the seven exceptions to the extension rule, shared by Fortran, Julia, R and WASM. The two content checks are for reading: mio_write and mio_convert's output go by the extension alone, so a .dat is written as Tecplot and a .mesh as Medit whatever the file being replaced holds (v16.17.0); pass "marc" to write a Marc deck) |
| Building | mio_mesh_set_points, mio_mesh_add_cell_block (int32 connectivity is widened to the core's int64), mio_mesh_add_point_data, mio_mesh_append_cell_data (one call per cell block, in block order), mio_mesh_add_field_data |
| Points/cells | mio_mesh_num_points, mio_mesh_point_dim, mio_mesh_get_points, mio_mesh_num_cell_blocks, mio_mesh_cell_block_info, mio_mesh_cell_block_type, mio_mesh_cell_block_conn |
| Named data | mio_mesh_num_{point,cell,field}_data, mio_mesh_{point,cell,field}_data_name (names in sorted order — identical on every backend), mio_mesh_get_{point,cell,field}_data, mio_mesh_cell_data_num_blocks |
| Version | MIO_VERSION_MAJOR / _MINOR / _PATCH and the ordered integer MIO_VERSION are compile-time macros in the header, with MIO_VERSION_AT_LEAST(major, minor, patch) / MIO_VERSION_BEFORE(...) for feature detection (#if MIO_VERSION_AT_LEAST(9, 5, 0) guards a call to mio_refine_ex, new in 9.5.0). mio_version() returns the version of the linked library instead, which with a shared build can genuinely differ from the header's — compile against the macros, report the call. A static_assert in c_api.cpp pins these to the C++ MESHIOPLUSPLUS_VERSION_* macros and CMake hard-fails if either disagrees with the project version, so the copies cannot drift |
| Isogeometric | mio_iga_refine, mio_iga_sample (an "mfem", "cad_json", "step" or "iges" mio_format_info in; a sampled mio_mesh and, optionally, a new handle with the refined or sampled model out; mio_iga_opts through mio_iga_opts_init) and mio_iga_evaluate (a mio_iga_evaluation read by mio_iga_evaluation_array with MIO_IGA_POINTS, _DERIVATIVES, _NORMALS, _INSIDE, _FIELD); see isogeometric layer |
| Operations | mio_extract_surface, mio_extract_skin, mio_compute_normals (mio_normals_opts / mio_normals_report; point and cell normals of a surface, optionally splitting vertices at creases — see normals), mio_feature_edges (mio_feature_edges_opts / mio_feature_edges_report, initialized by mio_feature_edges_opts_init; the sharp/open/non-manifold/inconsistent edges as a line mesh — see feature edges), mio_tessellate (mio_tessellate_opts / mio_tessellate_report, initialized by mio_tessellate_opts_init — levels 2, curved/fields on, so all-zero is not the default; NULL opts selects the defaults; isoparametric tessellation of triangle6/quad8/quad9/tetra10/hexahedron27 cells into triangles and tetrahedra with tessellate:source_point point data and tessellate:source_cell/tessellate:sub_index cell data, plus tessellate:stencil/tessellate:weights with record_stencil — see tessellation), mio_segment (mio_segment_opts / mio_segment_report, initialized by mio_segment_opts_init; smooth patches bounded by the feature edges, as segment:patch cell data, segment:adjacency field data and one cell region per patch — see segment), mio_fit_primitives (mio_fit_primitives_opts / mio_fit_primitives_report, initialized by mio_fit_primitives_opts_init; types is a bit mask of 1 << mio_fit_type, MIO_FIT_ALL_TYPES by default — see fit primitives), mio_fit_cad(mesh, step_path, &opts, &report) (mio_fit_cad_opts / mio_fit_cad_report, initialized by mio_fit_cad_opts_init; the fitted patches as a CAD solid with analytic faces, and, for what no primitive explains, with fillets rolling-ball fillet and blend faces (num_fillet, of which num_variable_fillet vary in radius and num_blend lie beside a freeform face; fit_cad:type 7), with threads helical thread faces (num_helical, of which num_rounded_thread have a profile that is not straight; type 8), since v17.2.0 with fillets chamfers (type 10), conic blends (type 7) and vertex blends (type 9) and with threads tapered and variable-pitch thread faces, counted in mio_fit_cad_report_ex (num_vertex_blend, num_chamfer, num_conic_blend, num_tapered_thread, num_variable_pitch_thread; 128 bytes, a reserved tail) through mio_fit_cad_ex(mesh, step_path, &opts, &report, &report_ex), as mio_fit_cad_report has no room left, and with freeform (freeform_degree, freeform_max_control; num_bspline; type 6) B-spline faces, a freeform patch folding over every flattening split into pieces with freeform_split (num_split_patches); freeform, freeform_split, fillets and threads are on after mio_fit_cad_opts_init but off in a zero-filled struct, as before they existed; written as STEP when step_path is non-empty; needs a library built -DMESHIOPLUSPLUS_WITH_OCCT=ON, which mio_has_occt() reports, and otherwise returns NULL with mio_last_error() naming the flag — see fit CAD), mio_hausdorff_distance(a, b, &opts, &report) (mio_hausdorff_opts / mio_hausdorff_report; see Hausdorff distance), region editing: mio_edit_regions(mesh, op, selectors, n, output, dim, tag, keep_inputs) (mio_region_op, mio_region_selector initialized by mio_region_selector_init, MIO_REGION_ANY for "any" / "inherit") and mio_mesh_remove_region(mesh, index) — see editing regions, periodic pairs: mio_match_periodic_nodes(mesh, &slave, &master, &opts) (mio_periodic_opts, identity matrix from mio_periodic_opts_init) → an opaque mio_periodic_pairs (mio_periodic_pairs_info, zero-copy _slave / _master / _unmatched borrows, _free) — see periodic node pairs, mio_attach_quality, mio_quality_counts, mio_check_quality(mesh, spec, max_inverted, max_degenerate, &report, buf, len) (the quality gate: mio_quality_gate_report with passed/counts/summary_length, the summary text into a caller buffer; MIO_OK whether or not it passed), mio_sniff_format, mio_compute_bandwidth, and reordering: mio_reorder (method "rcm"/"morton"/"hilbert") → mio_reorder_result (mio_reorder_result_mesh borrow, mio_reorder_result_node_perm / mio_reorder_result_num_cell_perms / mio_reorder_result_cell_perm zero-copy borrows, mio_reorder_result_take_mesh ownership transfer, mio_reorder_result_free), and comparison: mio_meshes_equal(a, b, atol, rtol, unordered, &equal) plus mio_diff(a, b, atol, rtol, unordered, &result) → mio_diff_result (mio_diff_result_verdict, mio_diff_result_point_summary, mio_diff_result_num_block_diffs / mio_diff_result_block, mio_diff_result_free), and merging: mio_merge(meshes, count, weld, atol, source_tag, data_policy, drop_duplicate_cells) (array of mesh handles → a new mio_mesh*; data_policy 0=intersection / 1=fill), and the editing/stats bundle: mio_transform(mesh, matrix[16], rotate_vector_data) → mio_mesh*; mio_clean(mesh, weld, atol, remove_orphans, drop_degenerate, drop_duplicate_cells, &n_welded, &n_orphan, &n_degen, &n_dup) → mio_mesh*; mio_crop_bbox(mesh, lo, hi, mode, record_ids) / mio_crop_plane(mesh, point, normal, mode, record_ids) (mode 0=all / 1=any) / mio_crop_predicate(mesh, array, compare, value, record_ids) (v9.25.0; keeps the cells whose scalar cell_data value satisfies a mio_refine_compare — the same vocabulary mio_refine_ex uses, evaluated by the same C++ function, and a non-finite cell value never matches whatever the comparison. There is deliberately no mode: bbox and half-space test points and then need an all/any rule, whereas a cell_data predicate is already one value per cell. Inside/outside a surface composes as mio_distance_to_surface at MIO_SDF_CENTER then this on "sdf:distance" < 0) → mio_mesh*; mio_split(mesh, by, tag_name) (by "type"/"component"/"region"/"tag"/"regions" — the last, added v8.7.0, is one piece per named Cell region and, uniquely, not a partition: a cell in several regions lands in several pieces, tag_name unused) → mio_split_result (mio_split_result_count / _key / _mesh borrow / _take_mesh / _free); mio_stats(mesh, &report) → fills a mio_stats_report struct (scalar geometric measures; per-cell-type counts are not carried); mio_convert_cells(mesh, mode, record_parent_ids) (mode "linearize"/"simplexify"/"elevate") → mio_convert_cells_result (mio_convert_cells_result_mesh borrow, _take_mesh ownership transfer, _point_map / _num_cell_maps / _cell_map zero-copy borrows, _free); mio_subdivide(mesh, record_parent_ids) (polyhedral refinement: one polyhedral child per face of every eligible 3D cell, connected to a new interior point -- no per-type template table, tabulated types and existing polyhedron blocks handled uniformly through the same detail::cell_rings/orient_rings machinery mio_gradient uses, and automatically conforming, unlike mio_refine) → mio_subdivide_result (mio_subdivide_result_mesh borrow, _take_mesh ownership transfer, _num_cell_maps / _cell_map zero-copy borrows, _free -- deliberately no point map, unlike mio_convert_cells_result, since subdivide never prunes or renumbers a point; fails with mio_last_error() naming the cell when its faces are not a closed orientable surface); mio_agglomerate(mesh, target_group_size) and mio_agglomerate_ex(mesh, &opts, &num_faces_merged, &num_rejected) (mio_agglomerate_opts, initialized by mio_agglomerate_opts_init: target_group_size, merge_coplanar_faces, coplanar_angle, min_sphericity — see agglomerate) (polyhedral coarsening, the many-to-one counterpart to mio_subdivide: greedy seed-and-grow over the mesh's shared-face dual, absorbing face-adjacent neighbours by accumulated shared-face area until each group reaches target_group_size members, then emitting one polyhedron per group whose faces are exactly its external boundary -- conserving volume exactly, since internal faces are simply dropped rather than re-triangulated) → mio_agglomerate_result (mio_agglomerate_result_mesh borrow, _take_mesh ownership transfer, _cell_map a single flat zero-copy borrow with no block argument and no _num_cell_maps accessor -- unlike every other opaque-result type here, an agglomerated cell's output index is a function of which group it joined, not which input block it came from -- _free; fails with mio_last_error() naming the face when the input is non-manifold; points are never pruned or renumbered, so there is no point map either, clean(..., remove_orphans=1) being the documented follow-up for a minimal point set); mio_refine(mesh, levels, record_parent_ids) (subdivision into same-type children) → mio_refine_result (the same accessor set: mio_refine_result_mesh borrow, _take_mesh, _point_map / _num_cell_maps / _cell_map, _free), plus mio_refine_ex(mesh, &opts) taking a mio_refine_opts (mio_read_opts' append-only reserved-tail discipline; zero-initialize through mio_refine_opts_init and it reproduces mio_refine exactly) with a cell selection — at most one of a global block-major index list, a region name, or a scalar cell_data predicate_array + predicate_op + predicate_value — a closure (MIO_REFINE_CLOSURE_REDGREEN; _PROPAGATE for the always-works baseline that reaches the whole edge-connected component; _BALANCED to keep the hanging nodes and only enforce 2:1 balance, which makes the output non-conforming and reports the constrained nodes in the refine:hanging point_data array) and record_levels for the refine:level array, plus record_hierarchy (v10.1.0; an int64_t taking one of the struct's former reserved slots, sizeof(mio_refine_opts) unchanged at 112 bytes) for the persistent refine:cell_id/refine:parent_id parent/child hierarchy a multigrid caller resolves across the sequence of meshes it keeps (a link between two meshes, not a tree inside one; an unsplit cell keeps its id and is its own parent, a split cell's children each get a fresh id and carry the parent's id) — it also forces refine:entity to be attached even when the closure leaves no hanging node, since that array already records the coarse corners each new fine node is the mean of; mio_partition(mesh, nparts, method, imbalance, mode, seed, record_ids, ghost_layers, weights_key) (method "sfc"/"kahip"/"auto"; NULL/"" strings mean the defaults) → mio_partition_result (mio_partition_result_num_pieces / _part_id / _mesh borrow / _take_mesh / per-piece zero-copy _point_map / _num_cell_maps / _cell_map / _free), plus mio_partition_labels(mesh, nparts, method, imbalance, mode, seed, weights_key, labels, labels_size), which fills a caller-allocated flat int64 buffer in block-major global cell order (labels_size must equal the total cell count; recover block alignment from the mesh's own per-block cell counts). method="kahip" in a build without MESHIOPLUSPLUS_WITH_KAHIP fails with mio_last_error() naming the option; and smoothing: mio_smooth(mesh, method, iterations, lambda, mu, fix_boundary, preserve_features, feature_angle, guard_inversion, &nodes_moved, &max_displacement, &skipped_inversion) (method "taubin"/"laplacian"; a negative lambda means the method's own default — 0.5 laplacian, 0.33 taubin) → a new mio_mesh* with the same cells and data and only its point coordinates moved, the three counter out-params being nullable exactly like mio_clean's; mio_smooth_opts/mio_smooth_ex (the mio_refine_opts/_ex pattern) add frozen/num_frozen — 0-based point ids to pin outright, range-checked, an out-of-range id failing the call by name — with mio_smooth itself exactly mio_smooth_ex with frozen unset; and green-element undo: mio_undo_green(coarse, fine, &num_groups_undone, &num_cells_removed) (a TWO-mesh function, like mio_interpolate — fine must carry one Int64 scalar cell_data array per block for each of refine:cell_id/refine:parent_id/refine:level, i.e. must come from an mio_refine_ex call with record_hierarchy AND record_levels both set; coarse must be the mesh that call was run on) → a new mio_mesh* restoring fine's transitional (green) sibling groups to the row coarse already has for them verbatim — a lookup, not a reconstruction, since mio_refine/mio_refine_ex never renumber or prune points — with the two counters nullable exactly like mio_smooth's; the six reserved refine:* arrays are always dropped from the output, and only a single-pass (levels=1) hierarchy is supported. Its per-block cell maps are not exposed on the flat ABI, a documented gap like mio_smooth's mFrozen; and mass-preserving field transfer: mio_conservative_interpolate(source, target, arrays, arrays_count, default_value, on_conflict) → mio_mesh* (a TWO-mesh function like mio_interpolate; unlike mio_interpolate, NULL/<= 0 arrays_count means every source point_data AND cell_data array — one algorithm regardless of location. Both meshes are simplexified first, which is what lets ragged/polyhedron blocks through for free. Output is always Float64, conserving sum(value * measure) over the region the two meshes share, unlike mio_interpolate's pointwise sampling; there is deliberately no extrapolate flag, since a silent fallback would break that guarantee); and the two cutters: mio_slice(mesh, origin[3], normal[3], record_parent_ids) → mio_mesh* (the planar cross-section, one dimension below the cut cells) and its data-driven sibling mio_isosurface(mesh, array_name, isovalues, n_isovalues, component, record_parent_ids) → mio_mesh* (the level set of a scalar point_data array; component negative = row magnitude, several isovalues land in one mesh tagged per cell with iso:value/iso:index, and a cell_data name fails with mio_last_error() naming mio_data_cell_to_point as the fix); and field differentiation: mio_gradient(mesh, array_name, op, method, location, output_name, component, overwrite, &num_skipped, &num_fallback) → mio_mesh* (the gradient, divergence or curl of a point_data array; op "gradient"/"divergence"/"curl", method "green-gauss"/"least-squares", location "cell"/"point", NULL/"" strings meaning the defaults, and the two counters nullable exactly like mio_clean's. component is negative for every component — deliberately the opposite of mio_isosurface's sentinel. An nc-component input yields 3*nc gradient components laid out [component][derivative]; divergence gives 1 and curl 3, both needing 2 or 3 components. Cells that cannot be differentiated yield NaN and are counted, and a cell_data name fails naming mio_data_cell_to_point); and second derivatives: mio_hessian(mesh, array_name, method, location, output_name, overwrite, &num_skipped, &num_fallback) → mio_mesh* (the Hessian of a scalar point_data field — mio_gradient's companion one order further, a composition of TWO mio_gradient calls, not a new kernel: the field is differentiated once at location="point" regardless of the caller's own location, then that (n,3) gradient is differentiated again with the default operator, producing (n,9) — the flattened row-major 3x3 Hessian. method is forwarded to BOTH internal passes; NULL/"" strings mean the defaults, and the two counters are nullable exactly like mio_clean's. A field that is at most linear has an exactly zero Hessian everywhere — the one mesh-shape-independent guarantee; a genuinely quadratic field's composition is exact on a structured/symmetric mesh away from its own boundary and approximate on an irregular mesh (see doc/hessian.md). Input must have exactly one component — a vector field's Hessian is a separate quantity per component, rejected naming the per-component workaround. A curvature-driven refinement indicator needs no new API: mio_data_calc(mesh, "curv = norm(`<array>:hessian`)", ...) on the 9-component output is exactly its Frobenius norm, ready for mio_refine_ex's predicate selector); and error estimation: mio_estimate_error(mesh, array_name, method, marking, marking_value, output_name, marked_name, overwrite, &global_error, &num_skipped, &num_marked) → mio_mesh* (the Zienkiewicz-Zhu recovery-based error indicator of a point_data field — a composition of mio_gradient with the measure-weighted point↔cell averaging round trip, not a new kernel — plus optional marking; method "zz" (the only one today), marking "none"/"absolute"/"fraction"/"dorfler", NULL/"" strings meaning the defaults, and all three counters nullable exactly like mio_clean's. error:zz (Float64) is always attached; error:marked (Int64 0/1) too when marking is not "none", so mio_refine_ex's own predicate_array/predicate_op/predicate_value needs no change at all to consume it. Cells that cannot be evaluated read NaN in error:zz and 0 (never NaN) in error:marked, counted in num_skipped and excluded from global_error and every marking policy's count); and field integration: mio_data_integrate_create(mesh, names, count) (NULL names = every cell_data array) → an opaque mio_data_integrate* — gradient's integration counterpart, a cell-measure-weighted total/mean, read with mio_data_integrate_count / _name (caller-buffer protocol) / _entry (fills a mio_field_integral_info{num_components, num_cells, num_skipped}, the whole-mesh entry) / _component(index, comp, &total, &mean, &domain_measure, &num_nan), plus the region axis _region_count / _region_name / _region_entry / _region_component (one independent entry per named Cell region present — not a partition), released with mio_data_integrate_free. A cell whose measure cannot be computed, or a component whose value is non-finite, is excluded from that component's numerator and denominator, never given a fallback weight of 1; a point_data-only name fails naming mio_data_point_to_cell as the fix. Read-only — no ABI bump, wholly new entry points |
| Data operations | Act on point_data/cell_data/field_data; the geometry is never modified (see data operations). Name lists cross as const char* const* names plus an explicit int64_t count (0 = every array). Array management: mio_data_drop / mio_data_keep / mio_data_rename → a new mio_mesh*. Averaging: mio_data_point_to_cell(mesh, names, count, suffix) / mio_data_cell_to_point(mesh, names, count, weight, suffix) (weight MIO_WEIGHT_UNIFORM/_MEASURE). Expressions: mio_data_calc(mesh, expression, location, output_name, overwrite). Conditioning: mio_data_condition(mesh, location, names, count, mode, lo, hi, scope, nan_policy, nan_replacement, suffix) with the MIO_COND_* / MIO_SCOPE_* / MIO_NAN_* enumerations. Tensor invariants: mio_tensor_invariants(mesh, location, names, count, outputs, prefix, suffix, overwrite) with the mio_tensor_invariant bitmask (MIO_TINV_MISES/_PRINCIPAL/_HYDROSTATIC/_DEVIATORIC/_ALL, OR together; 0 also means MIO_TINV_ALL). Summary: mio_data_info_create(mesh) → an opaque mio_data_info*, read with mio_data_info_count / _name (caller-buffer protocol) / _entry (fills a mio_data_array_info) / _component, released with mio_data_info_free |
| Interface topology | mio_region_adjacency(mesh, selectors, count) returns conforming shared facets between selected Cell regions, or cell blocks when selectors are empty and fewer than two Cell regions exist; output cell data carries parent cell/facet ids and per-facet measure — see region adjacency | | Settings pipeline | mio_pipeline_run_file(settings_path) / mio_pipeline_run_json(json_text) run a whole settings pipeline (read → operation chain → write; PascalCase ops/keys) and return mio_status — the flat ABI carries JSON text only, no step struct, and schema errors reach mio_last_error() naming the offending op/key. Needs a build with the JSON parser (-DMESHIOPLUSPLUS_WITH_JSON=ON, the default when the src/cpp/third_party/json submodule is checked out; off in the conan/vcpkg packages, whose source export has no submodule — the Eigen rule); otherwise the calls fail naming the flag, and mio_pipeline_has_json() reports which build this is. A structured report accessor is a recorded follow-up | | Sequences (transient) | mio_sequence_open(pattern) / _open_list / _count / _path / _step / _time / _time_source / _read / _free, plus mio_sequence_to_timeseries/_ex (fan-in), mio_timeseries_to_sequence (fan-out), mio_sequence_resample(seq, out_path, out_format, &opts) (mio_resample_opts, initialized by mio_resample_opts_init: the caller's times array, method 0 linear / 1 nearest / 2 previous, extrapolate 0 error / 1 clamp, blend_points; at most two source meshes live — see Resampling onto new times), mio_blend_steps(a, b, w, blend_points) (a new owned mesh) and mio_sequence_pipeline_run_file/_json. The handle is an ordered plan (paths, per-file step indices, times) and owns no mesh, so mio_sequence_read hands back an owned mio_mesh* you free — deliberately unlike mio_split_result_mesh's borrow, which would force the handle to cache everything it produced. That is the C ABI's expression of the streaming guarantee. mio_sequence_to_timeseries_ex takes a mio_write_opts* for the one option the transient writer has anywhere to put: MIO_ENCODING_ASCII selects XDMF's "XML" data format (no HDF5 needed) over the default "HDF" — Codec/FloatFormat fail by name, since the writer bypasses the registry and has nowhere to send them. See sequences. |
Four Python-only formats are absent here.
pmsh,zarr,caeandusd(v10.35.0, the physics-ML data path — see formats) are registered in the Python layer only, not in the sharedregistry.cppdispatch tables, so this ABI does not see them.
Every function is documented in the installed header, bindings/c/include/meshioplusplus/meshioplusplus.h.
The interface/contact API adds mio_region_adjacency, mio_find_interface, mio_contact_pairs, and mio_split_interface. Opaque result handles own their output; result-array accessors borrow until the matching *_result_free, while mio_find_interface_result_take_mesh transfers its mesh to the caller. All ABI ids are 0-based, including global cells and local facets; see interfaces and contact.
Format support
mio_read(path, "gmsh") reads non-periodic Gmsh 2.2, 4.0 and 4.1 files natively; 4.0 supports ASCII and binary with 4- or 8-byte producer counts. Periodic files require mio_read_with_info / mio_write_with_info, supporting output 4.1 (gmsh) or 2.2 (gmsh22). Info-less reads refuse $Periodic instead of discarding it. 4.0 and periodic metadata summaries fall back to a full read. See Gmsh.
MDPA side-channel membership: mio_mdpa_info_array(info, MIO_MDPA_SUBMODELPARTS, index, field, ...) exposes table ids at field 0, geometry ids at field 1 and constraint ids at field 2. These borrowed Int64 lists retain raw file ids, order and duplicates; mio_write_with_info restores them in nested parts. They are not remapped by mesh operations, and constraints remain opaque. The C ABI signatures and existing fields are unchanged; the underlying C++ layout uses ABI 21.
mio_write writes OpenRadioss starter decks (radioss, .rad) and MSC Marc decks (marc, by name) since v16.17.0, and OpenRadioss time-history files (radioss_th: one output per call from the radioss_th:* field data, by a file name like crashT01 or by name; points and cells are not stored, see the format page), Nastran OP2 results (nastran_op2, .op2: the mesh as GRID, element and property cards and one step of nodal and element results; see the format page) and LS-DYNA binouts (lsdyna_binout, by a file name binout or by name: nodout from the points and per-point arrays, other databases from binout:<database>:<variable> field data; see the format page) and Abaqus results files (abaqus_fil, .fil: the model and one increment of nodal and element results, ASCII, or binary with MIO_ENCODING_BINARY in mio_write_ex; see the format page) after those; radioss's placeholder materials and properties (stubs) are Python and C++ only. mio_write writes CalculiX results (frd, .frd, ASCII, the long I10 layout, one step) too; mio_write_ex with MIO_ENCODING_BINARY writes the binary layout ccx uses for *NODE OUTPUT (int32 ids, float64 coordinates, float32 results; see CalculiX results), and the short I5 form is Python and C++ only.
mio_write_ex takes a mio_write_opts* whose encoding is MIO_ENCODING_DEFAULT, _ASCII, _BINARY or, since v16.21.0, MIO_ENCODING_RAW_APPENDED (vtu only: raw binary arrays in one <AppendedData> section; any other format fails by name).
mio_write reaches the write-only step format (.step, .stp: a closed surface as a faceted CAD solid, no options — see STEP). mio_write reaches the gltf format (which mio_read also reads) (.glb binary, .gltf plus .bin) with default options. For parameterized export, initialize mio_gltf_opts with mio_gltf_opts_init and call mio_write_gltf(path, mesh, &opts): colour field/colormap/range, split angle, axis, scale and all native flags are available. Components are 0-based; component_set, vmin_set and vmax_set control optional values. String pointers are borrowed only for the call; NULL opts selects defaults. The new struct has its own reserved tail and leaves mio_write_opts unchanged.
PCD compressed binary uses mio_write_opts.codec = MIO_CODEC_LZF with mio_write_ex; it needs no optional library and rejects ASCII or non-PCD output. Native XDMF reads accept XDMF2/3 and absolute DataItem references; Netgen reads/writes carry names and periodic arrays (in native field_data) and support .vol.gz with zlib. See XDMF, Netgen and PCD.
All formats with a C++ implementation are available — the same set as the WASM binding plus, when the build found the dependencies, the HDF5-backed formats (cgns, h5m, hmf, med, vtkhdf, XDMF's HDF heavy-data path) and the netCDF-backed exodus. Two read-only formats need a library that is never on by default (v16.13.0): vtx (DOLFINx's ADIOS2 .bp directories) with -DMESHIOPLUSPLUS_WITH_ADIOS2=ON, and szplt (Tecplot SZL) with -DMESHIOPLUSPLUS_WITH_TECIO=ON and a TecIO you supply; without them mio_read names the missing library, as for HDF5. This includes the 2-D drawing formats svg and tikz (emitted with the fixed default styling — 3D meshes render their projected boundary skin with the default isometric camera; mio_read returns only the drawings meshio++ wrote, as planar meshes, and refuses a file without meshio++'s marker comment — mio_read_opts.lenient is strict=False). Probe at runtime with mio_format_readable()/mio_format_writable(); requesting a compiled-out format fails with a message naming the missing dependency. Formats that only exist in Python (neuroglancer, …) are not reachable from C. mdpa is reachable (since v9.0.0), but only its mesh-level blocks — the C++ reader declines an MDPA file using Tables/Geometries/Mesh blocks or non-empty Properties/ModelPartData metadata, naming the construct, since the C++ Mesh cannot carry them; see MDPA.
Parameterized writers use each format's Python-reference default (VTU: binary+zlib, gmsh: 4.1 binary, STL: ASCII, XDMF: HDF when built with HDF5 else XML, …). In particular stl/ply extract and write the boundary skin of a 3D volume mesh, matching the Python default (see Skin extraction); a standalone skin-extraction entry point is not part of the C API yet (documented follow-up). Per-call writer options are a possible future addition.
Native command-line binary
The core also ships a Python-free CLI — the same verbs as the Python command-line tool, built directly on the C++ core, so it needs neither a Python interpreter nor the pybind11 extension.
Prebuilt binaries: every GitHub Release tag (v*) ships a ready-to-run, statically-linked meshioplusplus executable for Linux (x86_64 and arm64), macOS (universal x86_64+arm64), and Windows (x86_64) — no install step, no runtime dependency to satisfy (no libstdc++/vcredist). These release binaries build with MESHIOPLUSPLUS_WITH_HDF5/NETCDF/ZLIB=OFF and the sequential parallel backend, trading those for a single dependency-free file (see .github/workflows/cli.yml); build from source (below) for the full HDF5/netCDF/multi-threaded feature set.
Build from source: off by default; build it alongside a standalone C++ tree:
build/configure.sh --cli --build # -DMESHIOPLUSPLUS_BUILD_CLI=ON
build/cpp-release/meshioplusplus --helpPass -DMESHIOPLUSPLUS_STATIC_RUNTIME=ON (alongside --cli, via a direct CMake configure) for the same statically-linked-runtime behavior as the release binaries: static libgcc/libstdc++ on GNU, static MSVC CRT (/MT) on Windows.
The installed executable is named meshioplusplus and mirrors the Python verbs:
meshioplusplus convert in.msh out.vtu
meshioplusplus info out.vtu
meshioplusplus ascii out.vtu # in-place; also: binary / compress / decompress
meshioplusplus quality mesh.vtu
meshioplusplus extract-surface vol.vtu surf.vtu
meshioplusplus reorder in.vtu out.vtu --method rcm --report
meshioplusplus diff a.vtu b.vtu # nonzero exit if different
meshioplusplus merge a.vtu b.vtu out.vtuFormat dispatch reuses the shared registry (with a content-sniff fallback on read); ASCII/binary/compress variants call the per-format writers directly. Named sets are regions in the core: info prints point/cell/side sets, diff compares them, and single-mesh convert -s/-d now converts point/cell membership to/from scalar integer data. There is no Python fallback, so a file whose C++ reader raises is not readable by the native CLI. It works under any mesh backend and any optional-dependency configuration; verbs that touch a compiled-out HDF5/netCDF format report the missing dependency by name.
mio_sets_to_data(mesh, location, data_name, join_char, order, count) returns a new mesh with labels and removes the converted regions. NULL names join set names with join_char (NULL means -); count=0 chooses native region-name order, otherwise the list must name every set once. mio_data_to_sets(mesh, location, key) converts a scalar integer field into regions and removes the field. Only MIO_DATA_POINT / MIO_DATA_CELL are accepted; errors return NULL with mio_last_error(). Free results with mio_mesh_free. See sets/data semantics.
Limitations (v1)
- Ragged cell blocks are carried (since v9.15.0): build one with
mio_mesh_add_polygon_block/mio_mesh_add_polyhedron_blockand read it back through themio_poly_conn_create/_get_shape/_nodes/_face_offsets/_cell_offsets/_freesnapshot.mio_mesh_cell_block_info_exreportsis_polyhedronalongside the counts, andmio_mesh_cell_block_connstill fails withMIO_ERR_UNSUPPORTEDbecause a ragged block has no rectangular buffer to borrow. See Polyhedra and ragged cells. Note the snapshot is an owning copy rather than a rule-3 borrow -- so, unusually here, it stays valid across mutating calls -- because theMESHIOmesh backend stored ragged blocks as nested vectors until v16.16.0, with no offsets array to point at, and theKRATOSbackend rebuilds its blocks lazily. The builders copy the three arrays straight into the mesh's own CSR storage. is_ragged's type differs between the two paths, a pre-existing wart kept for compatibility:mio_mesh_cell_block_infotakesint32_t*andmio_read_metadata_cell_blocktakesint*. The metadata path also reports nois_polyhedron, sincemio_read_metadata's per-block record is layout-frozen; create the mesh if you need it.- Named regions are carried (since v8.1.0):
mio_regions_create/_count/_name/_info/_entries/_freesnapshot a mesh's groups, andmio_mesh_add_regionadds one. Kinds areMIO_REGION_POINT/_CELL/_SIDE; cell entries are global block-major indices and side entries(cell, facet)pairs. See Named regions. Gmsh, Abaqus and MED (since v9.6.0 — one region perFAS/GROgroup name) map onto them fully (read + write); Exodus (since v8.6.0) reads them (element blocks/node sets/side sets) but does not yet write them. Since v8.7.0,mio_read_metadata_num_regions/_region_name/_region_info(the latter reusingmio_region_info's shape, minus entries) let a caller enumerate a mesh's regions frommio_read_metadata_createwithout a full read wherever the summary already comes from an in-memory mesh. - Remaining side-channel metadata is still dropped: MED's family ids/link names and mesh-level metadata (
mesh_name/description/unit_time/unit_coords— theMedInfofields regions don't carry; group names and membership are unaffected, see above), OpenFOAM cell tags, and theansysinpset channel pending its Phase-2 region mapping (UNV groups arrive as regions since v15.6.0). Use the Python API when you need those. - A
mio_meshhandle is not thread-safe; distinct handles may be used from distinct threads freely. That includes the formats on HDF5, netCDF, ADIOS2, TecIO and gidpost, which are not thread-safe themselves: the core serialises every call into them on one internal lock, so threads reading or writing such files queue instead of racing. A host that calls the samelibhdf5itself, outside meshio++, is not covered.
Selective reads and file summaries
mio_read is unchanged. mio_read_ex(path, format, opts) adds the selective-read options, and mio_read_metadata_create returns an opaque summary handle in the style of mio_split_result / mio_convert_cells_result / mio_subdivide_result / mio_agglomerate_result / mio_refine_result / mio_partition_result / mio_data_info.
mio_read_opts opts;
mio_read_opts_init(&opts); /* always -- never zero-fill by hand */
opts.points_only = 1;
opts.time_step = -1; /* the last step; 0 (the default) is the first */
mio_mesh* mesh = mio_read_ex("big.vtu", "vtu", &opts);
mio_read_metadata* meta = mio_read_metadata_create("run.exo", NULL);
int64_t np = mio_read_metadata_num_points(meta);
int fell_back = mio_read_metadata_fell_back(meta); /* 1 => the file was read in full */
/* How many time steps may `opts.time_step` name? */
int64_t nsteps = mio_read_metadata_num_time_values(meta);
double* times = malloc((size_t)nsteps * sizeof(double));
mio_read_metadata_time_values(meta, times, nsteps);
mio_read_metadata_free(meta);opts.time_step selects a step of a multi-step file (0 = the first, negative counts from the end); out of range fails the call rather than clamping. It was added in v8.6.0 by consuming one of the struct's six reserved int64_t slots, so sizeof(mio_read_opts) and every preceding field's offset are unchanged — a caller compiled against the older header still passes a correctly-sized object. This is the only sanctioned way to grow the struct; a gtest pins the tail's width.
A NULL opts.arrays means every array; a non-NULL pointer with num_arrays == 0 means none. That distinction is load-bearing and is preserved throughout.
mio_read_metadata_bbox returns MIO_ERR_NOT_FOUND when no bounding box was computed — the normal case for a native summary, which never decodes the point coordinates. It does not report a zero box.
ABI note: mio_read_opts is part of the installed library's permanent ABI. It carries reserved capacity and may only ever grow additively — never reorder, resize or repurpose an existing field. Always initialize through mio_read_opts_init() so fields added later default sensibly in code compiled against an older header.
Optional codecs in packages
Conan gains with_zstd / with_lz4 and vcpkg gains zstd / lz4 features, both off by default (unlike with_hdf5/with_netcdf/with_zlib). zlib remains the default codec, so existing package IDs and consumers are unaffected. See Compression codecs.
Software rendering (v16.34.0)
A deterministic software rasterizer and its terminal, HTML and PNG encodings, with no display or GPU: see terminal rendering for what is drawn and every option. The interactive terminal loop is CLI-only and has no entry point here.
mio_render_opts opts;
mio_render_opts_init(&opts); // an all-zero struct is NOT the default: width 0 is refused
opts.width = 800; opts.height = 600; opts.supersample = 2;
opts.color_by = "temperature"; opts.colorbar = 1; opts.clip_low = 2; opts.has_clip_low = 1;
mio_frame* f = mio_render(mesh, &opts); // NULL on failure, see mio_last_error()
const uint8_t* rgba = mio_frame_rgba(f); // width*height*4, straight alpha, row 0 on top
const int64_t* ids = mio_frame_cell_ids(f); // the input cell at each pixel, -1 for none
int64_t n = mio_frame_png(f, 0, NULL, 0); // length query, then the bytes into a buffer
mio_frame_free(f);
mio_text_opts text; mio_text_opts_init(&text); text.cols = 80; text.rows = 24;
int64_t len = mio_render_text(mesh, &opts, &text, buf, buflen); // string rule 5
mio_write_snapshot("part.png", mesh, &opts, NULL, NULL); // .png .txt .ansi .html .castCut-aways (v16.36.0, ABI 25). mio_render_opts gains cutaways (a borrowed pointer to num_cutaways planes of six doubles each: a point, then the normal of the side kept), num_cutaways (0 to 2) and cutaway_tint (0xRRGGBBAA, default 0xE88034FF), taking the place of two of the reserved words, so the struct is still 432 bytes and every earlier field is where it was. They clip the faces, lines and points against the planes at draw time and tint the back faces now in view; a count outside 0 to 2, a NULL pointer with a count, a zero normal or a non-zero reserved word is refused by name.
Streamlines (v16.37.0, ABI 25). mio_render_opts gains streamlines (the name of a vector point array, NULL or "" for none), stream_seeds (default 40), stream_length (the longest a line grows each way from its seed, in diagonals of the model; default 0.5) and stream_color (0xRRGGBBAA, default 0xF050A0FF), taking three more of the reserved words, so the struct is still 432 bytes, with one reserved word left. The lines follow the field over the mesh's own cells (triangles, quads, tetrahedra, hexahedra, wedges and pyramids) and are drawn like isolines; an unknown array, a seed count outside 1 to 10000 or a length outside (0, 10] is refused by name.
opts.streamlines = "velocity"; opts.stream_seeds = 60; opts.stream_color = 0xFFFFFFFF;const double plane[6] = {0.5, 0, 0, 1, 0, 0}; // keep x >= 0.5
opts.cutaways = plane; opts.num_cutaways = 1; opts.cutaway_tint = 0x0AC81EFF;mio_render_opts follows the usual rules (reserved tail, append-only growth; colours are packed 0xRRGGBBAA; enum values are the MIO_SHADING_*, MIO_EDGES_*, MIO_SCALE_* and MIO_DIAGNOSTIC_* macros; string fields are borrowed for the call and may be NULL). A frame is an owning mio_frame whose borrows (mio_frame_rgba, mio_frame_cell_ids) expire when it is freed; mio_frame_range gives the mapped range and mio_frame_note the notes (colour range, ticks and keys). A cell encoding of mio_frame_text needs a frame sized to its grid, which mio_render_text does for you. mio_detect_color_depth reads the values of NO_COLOR, COLORTERM and TERM. An unknown enum value, a bad option or a misnamed array is NULL/-1/an error status with the reason in mio_last_error().
v9.1.0 additions
Gmsh metadata. mio_read_with_info returns a "gmsh" handle containing 4.1 bounding-entity tags and 2.2/4.0/4.1 periodic links. mio_gmsh_info_count(info, section) accepts MIO_GMSH_BOUNDING_ENTITIES (per cell block) and MIO_GMSH_PERIODIC (per link); mio_gmsh_info_array(info, section, index, field, ...) borrows from the handle until it is freed. Bounding field 0 is Int32 signed entity tags. Periodic fields are 0 = Int32 [dimension, slave entity tag, master entity tag], 1 = Float64 affine coefficients (0 or 16), 2 = Int64 (N, 2) slave/master 0-based point rows. Entity tags stay file ids; ordering and duplicates survive I/O. The handle outlives its mesh, but operations do not remap the rows; rebuild the info after point/topology changes. mio_write_with_info accepts gmsh or gmsh22 for a Gmsh handle, writing binary with the usual defaults. Existing C signatures and option-struct layouts are unchanged.
MFEM NURBS patches (v16.40.0). For mfem, mio_read_with_info returns an "mfem" handle with the patches an MFEM NURBS mesh was sampled from (MFEM); a mesh that is not NURBS has none. mio_nurbs_info_count(info, section) takes MIO_NURBS_PATCHES, MIO_NURBS_FIELDS, MIO_NURBS_TOPOLOGY (one item, or none) and MIO_NURBS_CELL_MAP (one item); mio_nurbs_info_int, mio_nurbs_info_array (borrowed until mio_format_info_free) and mio_nurbs_info_string read item index, field field. A patch has int fields 0 = parametric dimension d and 1 = space dimension, and array fields 0 = Int32 degrees, 1 = Float64 control points (n, space dimension) (direction 0 fastest), 2 = Float64 weights, 3 + j = the knot vector of direction j and 6 + k = the Float64 (n, components) control values of field k, whose name is FIELDS string field 0. TOPOLOGY has int fields 0 = dimension and 1 = vertex count, and Int64 arrays 0 = patch vertices (patches, 2^d), 1 = patch attributes, 2 = boundary vertices, 3 = boundary attributes, 4 = edges (edges, 3); CELL_MAP has 0 = the patch of each element cell, 1 = its knot span per direction (cells, 3) and 2 = the boundary patch of each boundary cell. Everything is 0-based. mio_write_with_info(path, mesh, "mfem", info) writes the patches back as an MFEM NURBS mesh, and fails (MIO_ERR_WRITE) if the mesh is no longer their sampling. MIO_CELL_TYPES also gained the seven MIO_CELL_VtkBezier* values (ABI 26).
B-rep geometry (Kratos CAD JSON, STEP, IGES). For cad_json, mio_read_with_info returns a "cad_json" handle with the trimmed NURBS faces, trimming loops, edges and vertices the mesh was sampled from (Kratos CAD JSON); in a build with OpenCASCADE, step and iges return a "step"/"iges" handle of the same shape (STEP reading). mio_brep_info_count(info, section) takes MIO_BREP_FILE (one item), MIO_BREP_BREPS, MIO_BREP_PATCHES, MIO_BREP_FACES, MIO_BREP_LOOPS, MIO_BREP_TRIMS, MIO_BREP_EDGES, MIO_BREP_VERTICES and MIO_BREP_REFS; mio_brep_info_int, mio_brep_info_array (borrowed until mio_format_info_free) and mio_brep_info_string read item index, field field. Nested lists are flattened in file order, each owner naming its first item and its count: a face's int fields 6/7 are its first loop and number of loops, 8/9 its first trim and number of trims (its loops' trims, then its embedded edges); a loop's 1/2 its trims; an edge's or vertex's 2/3 (1/2 for a vertex) its topology entries in REFS. A patch has int fields 0/1 = the degrees and arrays 0 = Float64 control points (n, 3) (u fastest), 1 = weights, 2/3 = the full knot vectors; trims (their parameter curve) and edges (their 3-D curve) share the curve fields int 10 = degree, 11 = rational, 12 = reduced knots and arrays 10 = control points (n, 3), 11 = weights, 12 = full knots, 13 = Int64 ids, 14 = active range. Ids are -1 where the file names an entity by brep_name (string field 0), and the extra string fields hold the JSON members meshio++ does not model; the header documents every field. Everything is 0-based. mio_write_with_info(path, mesh, "cad_json", info) writes the B-rep back as CAD JSON, from a "step" or "iges" handle too.
mio_read_opts.lenient— downgrade "this reader cannot represent construct X" to a warning plus a skip (currently mdpa, vtkhdf's poly-vertex/poly-line/strip cells, andstrict=Falsefor thesvgandtikzdrawings: read one that lacks meshio++'s marker comment). It took a second formerreservedslot, sosizeof(mio_read_opts)and every preceding offset are unchanged. Seedoc/selective_read.md.mio_read_opts.piece/mio_read_opts.piece_set— keep one piece of a partitioned file (VTKHDF partitions or composite blocks):pieceis 0 for the first and negative from the end, and is honoured only whenpiece_setis nonzero (zero — including a hand-zeroed struct — merges every piece into one mesh with one cell region per piece). Out of range fails the call naming the piece count. They took the third and fourth formerreservedslots (reserved[4]→reserved[2]), sosizeof(mio_read_opts)and every preceding offset are unchanged. Seedoc/selective_read.md.mio_read_opts.drop_ghosts— nonzero removes the ghost cells (halo) of a partitioned.pvtu,.pvtpor.pvd: every cell with avtkGhostTypebit set, and the points only they used. Zero (the default) keeps them; every other reader ignores it. It took the fifth formerreservedslot (reserved[2]→reserved[1]), sosizeof(mio_read_opts)(80, now guarded by astatic_assertinc_api.cpp) and every preceding offset are unchanged. The C++ReadOptions::mGhostsbehind it is what took the C++ ABI to 15, so a consumer of the installed C++ headers rebuilds; the C API is unaffected. Seedoc/selective_read.md.- Transient XDMF:
mio_xdmf_series_create_ex+mio_xdmf_series_opts(append mode, auto-flush),mio_xdmf_series_flush,mio_xdmf_series_finalized, andmio_xdmf_series_write_data_arrays+mio_named_arrayfor writing a step from raw solver arrays.mio_xdmf_series_createis unchanged. Seedoc/xdmf_time_series.md.
Format side channels (v16.27.0). mio_read_with_info(path, format, opts, &info) reads a mesh and keeps what it cannot hold in an opaque mio_format_info handle (currently MDPA: tables, geometries, Mesh blocks, sub-model-part data, text ModelPartData and raw blocks such as Constraints); mio_write_with_info writes it back, and mio_format_info_free releases it. For a format with no side channel *info is NULL and the read is mio_read_ex's. The MDPA accessors take a section (MIO_MDPA_TABLES, MIO_MDPA_GEOMETRIES, …), an item index and a field: mio_mdpa_info_count, _string, _int, _array (borrowed, valid until mio_format_info_free) and _data_count/_kind/_string/_array for key/value entries. Point references come back 0-based; ids are the file's own. See MDPA. mio_read without an info refuses these blocks by name (or skips them with lenient). Properties bodies and entity names still ride the mesh and the registry as before; ModelPart, entity names and the Kratos bridge remain unreachable here, which is why the installable C++ API (MESHIOPLUSPLUS_INSTALL_CPP) exists.