C Low-level API
Manifests
Low-level C operations for manifest; concepts and integration code are documented separately.This page documents the transparent C functions behind the manifest concepts. New application integrations should start with the stable C API; this reference is for consumers of the advanced, structure-level API.
Loading architectures
Maelys DL has two loading families—manifest and inline—and four variants. The file-manifest variant has both a simple call and an extended diagnostic call, so this low-level API exposes five C functions in total. All four variants below are supported:
| Path | Variant | Low-level C call | Support |
|---|---|---|---|
| Manifest | file | maelys_datalog_manifest_load / maelys_datalog_manifest_load_ex | Supported |
| Manifest | buffer | maelys_datalog_manifest_load_from_text | Supported |
| Inline | static domain | maelys_datalog_load_policy_inline_with_static_domain | Supported |
| Inline | dynamic domain | maelys_datalog_load_policy_inline | Supported |
- Filemanifest_load / _ex
- Buffermanifest_load_from_text
- Static tableregister + load in one call
- Source bytesdomain registered first
The static inline call registers its compile-time predicate table and loads the policy in one operation. The other three variants use a domain registered beforehand. The stable opaque C API exposes only the file-manifest and dynamic-inline variants; it does not expose this buffer-manifest or one-call static-domain surface.
Capabilities by loading path
| Loading style | Metadata | Source bytes | Query whitelist | Use case |
|---|---|---|---|---|
| File manifest loading | Manifest JSON file | Policy files on disk | Manifest queries field | Production deployments. |
| Buffer manifest loading | Manifest JSON buffer | In-memory source bundle | Manifest queries field | Embedded, tests, streaming — any caller with in-memory sources. |
| Inline loading (dynamic) | Function arguments | In-memory src/src_len | None — all QUERY predicates accessible | Tests, examples, REPLs, simple embedded callers; the domain is registered separately, beforehand. |
| Inline loading (static) | Function arguments + compile-time predicate table | In-memory src/src_len | None — all QUERY predicates accessible | C static tables, WASM production builds, embedded C where the domain is compiled in — registers and loads in one call. |
Loading APIs
Maelys DL exposes five public loading APIs.
| API | Loading style | Use when |
|---|---|---|
maelys_datalog_manifest_load | File manifest | The manifest and policy files are on disk, and structured diagnostics are not needed. |
maelys_datalog_manifest_load_ex | File manifest | The manifest and policy files are on disk, and the caller wants structured diagnostics. |
maelys_datalog_manifest_load_from_text | In-memory manifest | The caller has manifest JSON and policy source bytes in memory, but still wants manifest metadata, modes, SHA verification, bundles, or multiple policies. |
maelys_datalog_load_policy_inline | In-memory inline source | The caller has one .dl policy source in memory and does not need manifest JSON, caller-provided SHA-256, bundles, modes, or multi-policy metadata. The domain is already registered. |
maelys_datalog_load_policy_inline_with_static_domain | In-memory inline source, static domain | The caller has a compile-time predicate table and wants to register the domain and load the policy in one call — no separate registration step. |
Example — document access
The low-level one-call static-domain loader uses the same
document_access_predicates table defined on
Registries
and the exact policy source shown in the
stable loader:
maelys_datalog_policy_set_t policy_set = {0};
maelys_datalog_diagnostic_t diagnostic = {0};
maelys_result_t rc = maelys_datalog_load_policy_inline_with_static_domain(
document_access_predicates,
sizeof(document_access_predicates) / sizeof(document_access_predicates[0]),
"documents", "documents.main", source, strlen(source),
0u, &policy_set, &diagnostic);
if (rc != MAELYS_OK) {
/* Inspect diagnostic; do not use policy_set. */
}This path registers the seven-predicate domain and loads one policy in one
operation. A separately registered document_access_domain_def can instead
be selected by maelys_datalog_load_policy_inline("documents", ...).
The policy source has no quoted literal, so neither path needs source atoms.
The other loading paths below remain available for manifest metadata and
multiple policies.
Choosing a loading path
Use file manifest loading when policies are deployed on disk.
Use buffer manifest loading when there is no filesystem, but the caller still wants the manifest model:
policy_set_id,policy_set_version- multi-policy loading
- loading modes
- caller-provided SHA-256 verification
- Public Query Whitelist
Use inline loading when the caller simply wants to load one .dl source already
in memory. Inline loading has no manifest Public Query Whitelist — all
predicates declared as QUERY by the selected domain are accessible.
Use the static domain variant specifically when the predicate table is already written as a compile-time C array and a separate registration call would just be extra ceremony — C static tables, WASM production builds, tests that want a one-call setup. Use the plain (dynamic) variant when the domain is registered separately, from JavaScript, from another module, or from a callback — anywhere the predicate table is not a literal C array sitting next to the load call.
File manifest loading
File manifest loading uses a manifest JSON file on disk. The manifest references policy source files by safe relative path, and the loader reads those files during loading.
Simple file variant
maelys_result_t
maelys_datalog_manifest_load(
const char *manifest_path,
unsigned flags,
maelys_datalog_policy_set_t *out_set);This is a convenience wrapper. It loads a manifest file without returning
structured diagnostics. Equivalent to calling the extended variant with
out_diag = NULL.
Extended file variant
maelys_result_t
maelys_datalog_manifest_load_ex(
const char *manifest_path,
unsigned flags,
maelys_datalog_policy_set_t *out_set,
maelys_datalog_diagnostic_t *out_diag);Use _ex when the caller needs to explain a loading failure precisely.
See Error codes for the full diagnostic code reference.
File loading example
#include "include/maelys_datalog.h"
maelys_result_t
load_document_access_from_files(void)
{
maelys_result_t rc;
maelys_datalog_policy_set_t policy_set;
maelys_datalog_diagnostic_t diag;
rc = document_access_domains_install();
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_manifest_load_ex(
"policies/manifest.json",
0,
&policy_set,
&diag);
if (rc != MAELYS_OK) return rc; /* fail closed */
/*
* policy_set is now ready. Pass policy_set.policies[0].symbols and
* .registry to edb_init() to initialize the runtime EDB.
* See: /docs/concepts/edb
*/
return MAELYS_OK;
}Buffer manifest loading
Buffer manifest loading uses the same manifest model, but avoids filesystem access. The caller provides manifest JSON bytes and source bundle entries.
maelys_result_t
maelys_datalog_manifest_load_from_text(
const char *manifest_json,
size_t manifest_json_len,
const maelys_datalog_policy_bundle_entry_t *bundle,
size_t bundle_count,
unsigned flags,
maelys_datalog_policy_set_t *out_set,
maelys_datalog_diagnostic_t *out_diag);The buffer loader:
- Parses the manifest JSON.
- For each enabled policy, finds the matching source entry by
policy_id. - Verifies the SHA-256 of the source bytes against the manifest.
- Installs the selected domain vocabulary.
- Validates and loads the policy source.
- Stores the
queriesPublic Query Whitelist in the returned policy set.
manifest_json does not need to be NUL-terminated. manifest_json_len is
authoritative.
In-memory policy sources
typedef struct {
const char *policy_id; /* matches policy_id in manifest JSON */
const char *src; /* .dl source bytes */
size_t src_len; /* byte length of src */
} maelys_datalog_policy_bundle_entry_t;Buffer loading example
#include <stdio.h>
#include "include/maelys_datalog.h"
maelys_result_t
load_document_access_from_buffers(void)
{
maelys_result_t rc;
maelys_datalog_policy_set_t policy_set;
maelys_datalog_diagnostic_t diag;
static const char policy_src[] =
"blocked(\"mallory\").\n"
"\n"
"allow(User, Document) :-\n"
" owns(User, Document),\n"
" not(blocked(User)),\n"
" sensitivity_level(User, Level),\n"
" Level >= 3.\n"
"\n"
"allow(User, Document) :-\n"
" shared_with(User, Document),\n"
" not(blocked(User)),\n"
" sensitivity_level(User, Level),\n"
" Level >= 3.\n";
rc = document_access_domains_install();
if (rc != MAELYS_OK) return rc;
/* Compute SHA-256 of the source bytes */
char sha256[65];
maelys_sha256_hex(
(const unsigned char *)policy_src,
sizeof(policy_src) - 1,
sha256);
/* Build manifest JSON — check for truncation */
char manifest[2048];
int written = snprintf(
manifest, sizeof(manifest),
"{"
"\"policy_set_id\":\"document_access.buffer\","
"\"policy_set_version\":\"1\","
"\"manifest_version\":\"1\","
"\"default_profile\":\"MAELYS-DATALOG-v2\","
"\"created_for\":\"test\","
"\"strict_loading\":true,"
"\"fail_closed\":true,"
"\"capabilities\":[],"
"\"policies\":[{"
"\"policy_id\":\"document_access.main\","
"\"domain\":\"document_access\","
"\"file\":\"ignored-for-buffer-loading.dl\","
"\"sha256\":\"%s\","
"\"mode\":\"shadow\","
"\"enabled\":true,"
"\"description\":\"buffer loading example\","
"\"queries\":[{\"name\":\"allow\",\"arity\":2}]"
"}]"
"}",
sha256);
if (written < 0 || (size_t)written >= sizeof(manifest))
return MAELYS_ERR_PAYLOAD_TOO_LARGE;
maelys_datalog_policy_bundle_entry_t sources[] = {
{
.policy_id = "document_access.main",
.src = policy_src,
.src_len = sizeof(policy_src) - 1,
},
};
rc = maelys_datalog_manifest_load_from_text(
manifest, (size_t)written,
sources, sizeof(sources) / sizeof(sources[0]),
0, &policy_set, &diag);
if (rc != MAELYS_OK) return rc;
/*
* policy_set now has allow/2 in its Public Query Whitelist.
* Pass policy_set.policies[0].symbols and .registry to edb_init()
* to initialize the runtime EDB for evaluation.
* See: /docs/concepts/edb
*/
return MAELYS_OK;
}Inline loading
Inline loading is the low-ceremony path for loading one policy directly from an
in-memory .dl source. It does not use a manifest.
maelys_result_t
maelys_datalog_load_policy_inline(
const char *domain,
const char *policy_id,
const char *src,
size_t src_len,
unsigned flags,
maelys_datalog_policy_set_t *out_set,
maelys_datalog_diagnostic_t *out_diag);Inline loading has no manifest Public Query Whitelist. All predicates
declared as QUERY by the selected domain are accessible through the query API.
There is no observation gate between the solver output and the caller.
Inline domain-closed vocabulary
All predicates used by the inline source must be declared by the selected domain. Inline loading does not support manifest-level overlays or per-call vocabulary extensions.
This is valid if the selected domain declares owns/2, sensitivity_level/2,
and allow/2:
allow(User, Document) :-
owns(User, Document),
sensitivity_level(User, Level),
Level >= 3.
This is invalid unless the selected domain also declares helper/2:
helper(User, Document) :- owns(User, Document).
allow(User, Document) :- helper(User, Document).
See Registries for the domain-closed vocabulary model.
Inline loading example
#include "include/maelys_datalog.h"
maelys_result_t
load_document_access_inline(void)
{
maelys_result_t rc;
maelys_datalog_policy_set_t policy_set;
maelys_datalog_diagnostic_t diag;
static const char policy_src[] =
"allow(User, Document) :-\n"
" owns(User, Document),\n"
" sensitivity_level(User, Level),\n"
" Level >= 3.\n";
rc = document_access_domains_install();
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_load_policy_inline(
"document_access",
"document_access.inline",
policy_src,
sizeof(policy_src) - 1,
0,
&policy_set,
&diag);
if (rc != MAELYS_OK) return rc;
/*
* All domain QUERY predicates are accessible — no whitelist.
* Pass policy_set.policies[0].symbols and .registry to edb_init()
* to initialize the runtime EDB for evaluation.
* See: /docs/concepts/edb
*/
return MAELYS_OK;
}Inline loading with a static domain
maelys_datalog_load_policy_inline_with_static_domain registers a domain from
a compile-time predicate table and loads a policy in one call — no separate
registration step, no separate domain_registry_register call to write.
maelys_result_t
maelys_datalog_load_policy_inline_with_static_domain(
const maelys_datalog_predicate_def_t *predicates,
size_t predicate_count,
const char *domain_name,
const char *policy_id,
const char *src,
size_t src_len,
unsigned flags,
maelys_datalog_policy_set_t *out_set,
maelys_datalog_diagnostic_t *out_diag);Verified against the implementation: this function is a thin wrapper — it
calls maelys_datalog_domain_registry_register with predicates/
predicate_count/domain_name, then calls maelys_datalog_load_policy_inline
with the remaining arguments unchanged, flags included. Every constraint of
the dynamic variant (flags must be 0, domain-closed vocabulary, no
Public Query Whitelist) applies identically here.
domain_name and predicates are stored by pointer in the global domain
registry (first-registration-wins, see
Registries — Registering a domain)
and must remain valid for as long as the registry may reference them — a
static const array and string literals satisfy this automatically, which is
why this API exists specifically for compile-time C tables rather than
heap-built or short-lived ones.
Static domain loading example
Reuses the document_access_predicates static table from
Registries — Method 1: static predicate table.
#include "include/maelys_datalog.h"
maelys_result_t
load_document_access_static(void)
{
maelys_result_t rc;
maelys_datalog_policy_set_t policy_set;
maelys_datalog_diagnostic_t diag;
static const char policy_src[] =
"allow(User, Document) :-\n"
" owns(User, Document),\n"
" sensitivity_level(User, Level),\n"
" Level >= 3.\n";
rc = maelys_datalog_load_policy_inline_with_static_domain(
document_access_predicates,
sizeof(document_access_predicates) / sizeof(document_access_predicates[0]),
"document_access",
"document_access.static",
policy_src,
sizeof(policy_src) - 1,
0,
&policy_set,
&diag);
if (rc != MAELYS_OK) return rc; /* fail closed */
/*
* No separate document_access_domains_install() call needed above —
* this one call registered the domain and loaded the policy.
* Pass policy_set.policies[0].symbols and .registry to edb_init()
* to initialize the runtime EDB.
* See: /docs/concepts/edb
*/
return MAELYS_OK;
}WASM loading
For WASM and embedded environments, use the in-memory loading paths. See WASM bindings for the full build and API reference.
The JavaScript binding provides registerDomain and loadPolicy through the
public SDK. It does not export the native manifest loader or the historical
static-domain target. In-memory bundles remain separate binding work.
What loading returns
Every loading function writes its result into out_set on success. The
policy_set you receive is not just a return value — it is the shared context
that every subsequent operation depends on.
maelys_datalog_policy_set_t policy_set; /* filled by any loader */
policy_set.policies[0] /* first loaded policy (one per manifest entry) */
.symbols /* shared string intern table: "alice" → ID 1 */
.registry /* predicate registry: which predicates exist */
/* ... parsed rules, strata, query whitelist, identity ... */These two fields are the bridge between loading and runtime evaluation. The EDB must share the same symbol table and predicate registry as the policy it will be solved against. If they used different tables, string IDs would not match between EDB facts and ruleset rules — no fact would ever match a rule.
/* After loading, initialize the EDB with the policy's shared context */
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t pool[MAELYS_DATALOG_MAX_EDB_FACTS];
maelys_datalog_edb_init(
&edb,
pool,
MAELYS_DATALOG_MAX_EDB_FACTS,
&policy_set.policies[0].symbols, /* same string table as the ruleset */
&policy_set.policies[0].registry); /* same predicate vocabulary */From this point, loading is complete. The full evaluation lifecycle is:
1. Load — manifest loading produces policy_set
2. Init — edb_init(&edb, pool, N, &policy_set.policies[0].symbols,
&policy_set.policies[0].registry)
3. Add — edb_add_fact(&edb, "owns", terms, 2) per request
4. Finalize — edb_finalize(&edb)
5. Solve — solve_once(&policy_set.policies[0], &edb, &result)
6. Query — query_solved_ground_fact(result, "allow", terms, 2, &present)
7. Free — solve_result_free(result)
8. Repeat — edb_clear(&edb), go to step 3 for the next requestThe ruleset (policy_set.policies[0]) is loaded once and reused across all
requests. Only the EDB changes per request.
See Runtime EDB — initialization for a detailed explanation of the EDB lifecycle and why the shared symbol table is required.
Parameter reference
| Function | Whitelist enforced | Diagnostics |
|---|---|---|
maelys_datalog_manifest_load | Yes — manifest queries | No |
maelys_datalog_manifest_load_ex | Yes — manifest queries | Optional out_diag |
maelys_datalog_manifest_load_from_text | Yes — manifest queries | Optional out_diag |
maelys_datalog_load_policy_inline | No — all QUERY accessible | Optional out_diag |
maelys_datalog_load_policy_inline_with_static_domain | No — all QUERY accessible | Optional out_diag |
Shared output parameters
| Name | Type | Meaning |
|---|---|---|
out_set | maelys_datalog_policy_set_t * | Receives the loaded policy set on success. Cleared on failure. Must not be NULL. |
out_diag | maelys_datalog_diagnostic_t * | Optional structured diagnostic output. May be NULL for APIs that expose it. |
File manifest parameters
| Name | Type | Meaning |
|---|---|---|
manifest_path | const char * | Filesystem path to the manifest JSON file. Must not be NULL. |
flags | unsigned | Manifest loading flags. Use 0 for strict production loading. |
out_set | maelys_datalog_policy_set_t * | Receives the loaded policy set. Cleared on failure. |
out_diag | maelys_datalog_diagnostic_t * | Optional diagnostics. Only on _ex. May be NULL. |
Buffer manifest parameters
| Name | Type | Meaning |
|---|---|---|
manifest_json | const char * | Manifest JSON bytes. Does not need to be NUL-terminated. |
manifest_json_len | size_t | Authoritative byte length of manifest_json. |
bundle | const maelys_datalog_policy_bundle_entry_t * | In-memory .dl source entries matched by policy_id. May be NULL if bundle_count is zero. |
bundle_count | size_t | Number of entries in bundle. |
flags | unsigned | Manifest loading flags. Use 0 for strict production loading. |
out_set | maelys_datalog_policy_set_t * | Receives the loaded policy set. Cleared on failure. |
out_diag | maelys_datalog_diagnostic_t * | Optional diagnostics. May be NULL. |
Inline (dynamic) parameters
| Name | Type | Meaning |
|---|---|---|
domain | const char * | Registered domain name. Non-NULL, non-empty. |
policy_id | const char * | Stable identifier for this inline policy. Non-NULL, non-empty. |
src | const char * | .dl policy source bytes. Does not need to be NUL-terminated. |
src_len | size_t | Authoritative byte length of src. Must be greater than zero. |
flags | unsigned | Reserved. Must be 0. |
out_set | maelys_datalog_policy_set_t * | Receives the loaded single-policy set. Cleared on failure. |
out_diag | maelys_datalog_diagnostic_t * | Optional diagnostics. May be NULL. |
Inline (static domain) parameters
Same as the dynamic variant, plus the predicate table registered before loading:
| Name | Type | Meaning |
|---|---|---|
predicates | const maelys_datalog_predicate_def_t * | Compile-time predicate table. Stored by pointer in the domain registry — must remain valid for the registry's lifetime. |
predicate_count | size_t | Number of entries in predicates. |
domain_name | const char * | Domain name to register. Stored by pointer — same lifetime requirement as predicates. |
policy_id | const char * | Stable identifier for this inline policy. Non-NULL, non-empty. |
src | const char * | .dl policy source bytes. Does not need to be NUL-terminated. |
src_len | size_t | Authoritative byte length of src. Must be greater than zero. |
flags | unsigned | Reserved. Must be 0 — passed through unchanged to the same check load_policy_inline performs. |
out_set | maelys_datalog_policy_set_t * | Receives the loaded single-policy set. Cleared on failure. |
out_diag | maelys_datalog_diagnostic_t * | Optional diagnostics. May be NULL. |
Flags
| Loading style | Accepted flags | Meaning |
|---|---|---|
| Manifest loading | 0 | No optional permissions: enabled test-only policies are rejected and policy string constants must be declared in the domain. |
| Manifest loading | MAELYS_DATALOG_MANIFEST_ALLOW_TEST_ONLY | Allows enabled test_only policies to load and evaluate normally; it does not relax atom declarations. Tests and development only. |
| Manifest loading | MAELYS_DATALOG_MANIFEST_ALLOW_UNDECLARED_POLICY_ATOMS | Allows policy-local source constants absent from the domain's atoms; it does not admit test-only policies. |
| Manifest loading | Both permission bits combined with | | Grants the two permissions independently; other checks still apply. |
| Inline loading (dynamic) | 0 | Required. |
| Inline loading (dynamic) | Any non-zero value | Rejected with MAELYS_ERR_INVALID_ARGUMENT. |
| Inline loading (static domain) | 0 | Required — passed through unchanged to the same check as the dynamic variant. |
| Inline loading (static domain) | Any non-zero value | Rejected with MAELYS_ERR_INVALID_ARGUMENT. |
These are independent permissions, not strict/permissive modes. atoms belongs to the registered domain, not the manifest. A source fact such as blocked("mallory"). needs the string declared there unless the caller grants the source-constant permission. The same value supplied in a request EDB needs no atom declaration. Neither permission creates predicates or changes the domain globally.
The table above describes the lower-level loading API. The opaque facade uses MAELYS_DATALOG_PUBLIC_ALLOW_TEST_ONLY and MAELYS_DATALOG_PUBLIC_ALLOW_UNDECLARED_POLICY_ATOMS instead. Its manifest loader rejects unknown bits with MAELYS_DATALOG_STATUS_INVALID_ARGUMENT. Its maelys_datalog_policy_load_inline() has no flags parameter, unlike the lower-level inline signatures on this page; it always requires declared policy constants. See public loading permissions.
Diagnostics
The return code tells the caller the broad class of failure. The diagnostic object explains the precise cause.
A manifest SHA mismatch:
Return code : MAELYS_ERR_INVALID_FIELD
Diagnostic : code=MAELYS_DATALOG_DIAG_MANIFEST_SHA_MISMATCH
field=sha256 / hint=update manifest sha256 after policy editA rejected test-only policy:
Return code : MAELYS_ERR_FORBIDDEN
Diagnostic : code=MAELYS_DATALOG_DIAG_MANIFEST_TEST_ONLY_REJECTEDA Public Query Whitelist validation failure:
Return code : MAELYS_ERR_INVALID_FIELD
(unknown predicate, arity mismatch, or missing QUERY flag)A non-whitelisted query at runtime:
Return code : MAELYS_ERR_FORBIDDEN
(predicate exists but is not in the Public Query Whitelist)See Error codes for the full diagnostic code reference.
Return values
| Return code | Meaning |
|---|---|
MAELYS_OK | Loading succeeded. |
MAELYS_ERR_INVALID_ARGUMENT | A required argument is invalid. For inline loading, also includes non-zero flags. |
MAELYS_ERR_NOT_FOUND | Manifest file, referenced policy file, or bundle entry not found. |
MAELYS_ERR_INVALID_FIELD | Invalid manifest JSON, policy source, or Public Query Whitelist validation failure. |
MAELYS_ERR_UNSUPPORTED | Unknown domain. |
MAELYS_ERR_FORBIDDEN | Test-only policy without flag, or non-whitelisted query at runtime. |
MAELYS_ERR_IO | File read failure. File manifest loading only. |
MAELYS_ERR_INTERNAL | Internal allocation failure. |
MAELYS_ERR_PAYLOAD_TOO_LARGE | Static capacity or bounded field size exceeded. |