client library - mvx-lang/mvx GitHub Wiki
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 myprogTwo 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.
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.
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.
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 */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.
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.
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;
}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.
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.
#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;
}-
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,
CALLarguments 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.