client library - mvx-lang/mvx GitHub Wiki

The client library — driving an account from C

libmvxc lets a program outside MVX open an account and work in it: records, files, select lists, subroutine calls, TCL sentences. It is the surface mv-connect is built on, and the surface a Python, PHP or Go binding should be built on — the same shape those languages already expect from mysql or pgsql.

One header, one library:

#include <mvxc.h>
$ cc -I$MVXHOME/include myprog.c -L$MVXHOME/lib -lmvxc -Wl,-rpath,$MVXHOME/lib -o myprog

Two promises shape the whole thing:

  • Every call is a request and a response. Nothing is left half-done between calls, so an implementation that puts a network in the middle can keep the same interface.
  • Nothing here aborts your program. A failure is a status you read, never an exit. That is the opposite of the runtime's own habit, and deliberate: a library that killed its host because one allocation failed would be unusable.

The header is extern "C"-wrapped, so C++ can include and link it as it stands.


The one rule to know

Every const char * an accessor gives you belongs to the value it came from, and stays valid until that value is modified or freed.

Two reads together are therefore fine — that is the point:

printf("%s %s\n", mvxc_attr(r, 1), mvxc_attr(r, 2));      /* fine */

But do not mix a read and a write of the same value in one expression. C does not specify the order arguments are evaluated, so this may read the capture before the execute fills it, and then free that string when the execute runs:

printf("%d %s", mvxc_execute(s, "X", cap), mvxc_str(cap));   /* WRONG */

Split the statement. This has caught the author of the rule twice, in two different tests, so treat it as a property of the API rather than a mistake other people make.


The session

mvxc_status st;
mvxc_session *s = mvxc_connect("/path/to/account", &st);
if (!s) { /* st says why */ }
...
mvxc_disconnect(s);

NULL for the account takes the process default — $MVXACCOUNT, else the working directory — the same one a verb would get.

Connecting with an account name enters it, which runs its LOGIN. A VOC LOGIN entry executes exactly as a LOGTO would run it, because that is what this is: an account's LOGIN is how it makes itself usable, and a session that skipped it would be subtly different from every other way in. Passing NULL does not log to anywhere, so it does not run one.

A LOGIN runs at @LEVEL 1, never 0. A client is a running program, not a prompt: the C caller owns the screen, so an interactive routine can tell it does not and decline. @LEVEL 0 means "nothing is above me, I own the terminal", and nothing reached through this library may answer that.

One session is one MV session: its select list, its locks, its account.

mvxc_error(s) gives the last failure as text, "" when there has not been one.


Values — MV data, kept as MV data

A value is an opaque handle wrapping one MV dynamic array, so attributes, multivalues and subvalues stay what they are instead of being flattened into a C string and re-parsed.

mvxc_new() an empty value ("", not unassigned)
mvxc_new_str(s), mvxc_from_bytes(p, n) from text, or from bytes that may contain marks
mvxc_free(v) done with it
mvxc_str(v) the whole thing
mvxc_bytes(v, &n) the whole thing with @AM/@VM/@SM intact, and its length
mvxc_attr(v, a) attribute a
mvxc_val_at(v, a, m) value m of attribute a
mvxc_sub(v, a, m, s) subvalue s of that
mvxc_dcount(v, a) how many — attributes when a is 0, values in a otherwise
mvxc_set, mvxc_set_attr, mvxc_set_val replace
mvxc_ins_val, mvxc_del_val insert, delete
mvxc_val *r = mvxc_new();
mvxc_set_attr(r, 1, "Widget");
mvxc_set_attr(r, 2, "9.99");
mvxc_set_val (r, 3, 1, "red");
mvxc_set_val (r, 3, 2, "blue");      /* attribute 3 is now red]blue */

Records

mvxc_file *f = mvxc_open(s, "PARTS", NULL, &st);   /* "DICT" for its dictionary */
mvxc_read(f, id, &st) a new value you own and free; NULL with MVXC_NOTFOUND is a miss, not an error
mvxc_read_into(f, id, dst) the same read into a value you already have
mvxc_readu(f, id, wait, &st) read with a lock; wait says whether to block
mvxc_write(f, id, rec)
mvxc_delete(f, id)
mvxc_release(f, id) drop a lock without writing
mvxc_close(f)

Use mvxc_read_into in a loop. mvxc_read allocates a value per record; read_into reuses one, which is the difference between a malloc per record and none — and walking an account is what a consumer of this library mostly does. On a miss dst keeps what it held, so a caller that checks the status cannot read stale content by accident.

Select lists

mvxc_select(f);
const char *id;
while ((id = mvxc_next(s)) != NULL) { ... }

The id belongs to the session and is replaced by the next call — that is what a remote cursor can honestly promise. Copy it if you need to keep it.

Files

mvxc_create_file(s, name, type) NULL for the account default, "DIR" for a directory file
mvxc_delete_file(s, name)
mvxc_files(s) the account's files as an ordinary value: one attribute per file, name and type as its first two values
mvxc_bind_file(s, file, want, instead, cap) put a file on a named backend

mvxc_files needs no cursor and no new type, because what you already have reads it exactly as it reads a record:

mvxc_val *fl = mvxc_files(s);
for (int i = 1; i <= mvxc_dcount(fl, 0); i++)
    printf("%-20s %s\n", mvxc_val_at(fl, i, 1), mvxc_val_at(fl, i, 2));
mvxc_free(fl);

mvxc_bind_file is what a checkout needs when a record's %FILE% control names the backend that file was on, so it lands there instead of on this host's default. The library does not ask — the caller owns the screen — so when the host has no such backend it hands the decision back:

char instead[64];
switch (mvxc_bind_file(s, "PARTS", "postgres", instead, sizeof instead)) {
case MVXC_OK:       break;                    /* bound, or already the default */
case MVXC_NOTFOUND: /* no such backend here; `instead` is what this host
                       would offer.  Ask your user, then call again with it. */
    break;
default:            /* $MVXDRIVER names a backend this host does not have */
    break;
}

Running things

mvxc_val *argv[2] = { mvxc_new_str("21"), mvxc_new() };
mvxc_call(s, "DOUBLE.IT", 2, argv);        /* argv[1] now holds 42 */

mvxc_val *cap = mvxc_new();
mvxc_execute(s, "LIST PARTS", cap);        /* one attribute per line */

mvxc_call passes arguments in and out as BASIC does: a subroutine's changes are visible after the call returns, never during it. Saying so now is what lets a remote implementation copy them across a wire.

mvxc_cataloged(s, name) asks whether a CALL would resolve. It is separate because a caller usually wants to choose rather than be told afterwards — and on jBASE an unresolved CALL traps into the debugger instead of returning, so asking first is the portable habit.


Status

MVXC_OK 0
MVXC_NOTFOUND no such record, file or subroutine
MVXC_LOCKED held by another session
MVXC_DENIED the runtime privilege gate refused
MVXC_ERROR anything else — mvxc_error() says what

mvxc_version() reports the toolchain's version. A binding should surface it: "which mvx" is usually the first question about a bug.


A whole program

#include <mvxc.h>
#include <stdio.h>

int main(void) {
    mvxc_status st;
    mvxc_session *s = mvxc_connect(NULL, &st);
    if (!s) { fprintf(stderr, "cannot connect\n"); return 1; }

    mvxc_file *f = mvxc_open(s, "PARTS", NULL, &st);
    if (!f) { fprintf(stderr, "open: %s\n", mvxc_error(s)); return 1; }

    mvxc_val *r = mvxc_new();
    mvxc_set_attr(r, 1, "Widget");
    mvxc_set_attr(r, 2, "9.99");
    mvxc_write(f, "W1", r);

    /* one value, reused for every record */
    mvxc_select(f);
    const char *id;
    while ((id = mvxc_next(s)) != NULL) {
        if (mvxc_read_into(f, id, r) != MVXC_OK) continue;
        printf("%-8s %-12s %s\n", id, mvxc_attr(r, 1), mvxc_attr(r, 2));
    }

    mvxc_free(r);
    mvxc_close(f);
    mvxc_disconnect(s);
    return 0;
}

What is not here yet

  • A remote transport. Everything above runs in-process, against an account on this host. The interface was shaped so that a network can go in the middle without changing it — every call a request and a response, the select cursor owned by the session, CALL arguments copied rather than shared — but the wire itself is not written.
  • A C++ wrapper. The header is extern "C" so C++ can use it as it stands; an idiomatic wrapper (a value type that frees itself, a session that closes itself, iteration that reads like iteration) is #304.
  • Bindings. Python, PHP and Go are the obvious ones, and are what the LIST and SORT shape was designed for.

#289 tracks the library as a whole.

⚠️ **GitHub.com Fallback** ⚠️