shithub: lu9-p9

Download patch

ref: 3654451f1948e59b1f99d7bb6efb1283714d246d
author: kvik <kvik@a-b.xyz>
date: Thu Apr 8 18:44:21 EDT 2021

Feed the git

diff: cannot open b/mod//null: file does not exist: 'b/mod//null'
--- /dev/null
+++ b/fs.c
@@ -1,0 +1,122 @@
+static int
+p9_open(lua_State *L)
+{
+	const char *file;
+	int mode;
+	int fd;
+	
+	file = luaL_checkstring(L, 1);
+	mode = luaL_checkinteger(L, 2);
+	if((fd = open(file, mode)) == -1)
+		lerror(L, "open");
+	lua_pushinteger(L, fd);
+	return 1;
+}
+
+static int
+p9_create(lua_State *L)
+{
+	const char *file;
+	int fd, mode;
+	ulong perm;
+	
+	file = luaL_checkstring(L, 1);
+	mode = luaL_checkinteger(L, 2);
+	perm = luaL_checkinteger(L, 3);
+	if((fd = create(file, mode, perm)) == -1)
+		lerror(L, "create");
+	lua_pushinteger(L, fd);
+	return 1;
+}
+
+static int
+p9_close(lua_State *L)
+{
+	if(close(luaL_checkinteger(L, 1)) == -1)
+		lerror(L, "close");
+	return 0;
+}
+
+static int
+p9_read(lua_State *L)
+{
+	lua_Integer fd, nbytes, offset;
+	long n;
+	char *buf;
+	
+	fd = luaL_checkinteger(L, 1);
+	nbytes = luaL_checkinteger(L, 2);
+	offset = luaL_optinteger(L, 3, -1);
+	buf = getbuffer(L, nbytes);
+	if(offset == -1)
+		n = read(fd, buf, nbytes);
+	else
+		n = pread(fd, buf, nbytes, offset);
+	if(n == -1)
+		lerror(L, "read");
+	lua_pushlstring(L, buf, n);
+	return 1;
+}
+
+static int
+p9_write(lua_State *L)
+{
+	lua_Integer fd, offset;
+	size_t nbytes;
+	const char *buf;
+	long n;
+
+	fd = luaL_checkinteger(L, 1);
+	buf = luaL_checklstring(L, 2, &nbytes);
+	nbytes = luaL_optinteger(L, 3, nbytes);
+	offset = luaL_optinteger(L, 4, -1);
+	if(offset == -1)
+		n = write(fd, buf, nbytes);
+	else
+		n = pwrite(fd, buf, nbytes, offset);
+	if(n != nbytes)
+		lerror(L, "write");
+	lua_pushinteger(L, n);
+	return 1;
+}
+
+static int
+p9_seek(lua_State *L)
+{
+	lua_Integer fd, n, type;
+	vlong off;
+	
+	fd = luaL_checkinteger(L, 1);
+	n = luaL_checkinteger(L, 2);
+	type = luaL_checkinteger(L, 3);
+	if((off = seek(fd, n, type)) == -1)
+		lerror(L, "seek");
+	lua_pushinteger(L, off);
+	return 1;
+}
+
+static int
+p9_remove(lua_State *L)
+{
+	const char *file;
+	
+	file = luaL_checkstring(L, 1);
+	if(remove(file) == -1)
+		lerror(L, "remove");
+	lua_pushboolean(L, 1);
+	return 1;
+}
+
+static int
+p9_fd2path(lua_State *L)
+{
+	lua_Integer fd;
+	char *buf;
+	
+	fd = luaL_checkinteger(L, 1);
+	buf = getbuffer(L, 8192);
+	if(fd2path(fd, buf, 8192) != 0)
+		lerror(L, "fd2path");
+	lua_pushstring(L, buf);
+	return 1;
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,30 @@
+</$objtype/mkfile
+
+MOD=p9
+MODPATH=/sys/lib/lua
+
+CFLAGS=-FTVw -p -I../lua/shim -I../lua
+
+LIB=libp9.a.$O
+
+OBJS=p9.$O
+
+all:V: $LIB
+
+install:V: all
+	if(~ $#luav 0)
+		luav=`{lua9 -v}
+	for(p in $MODPATH/$luav)
+		mkdir -p $p/$MOD && dircp mod $p/$MOD
+
+clean:V:
+	rm -f *.[$OS] *.a.[$OS]
+
+$LIB: $OBJS
+	ar cr $target $prereq
+
+%.$O: %.c
+	$CC $CFLAGS $stem.c
+
+p9.$O: p9.c fs.c proc.c
+
--- /dev/null
+++ b/mod/init.lua
@@ -1,0 +1,441 @@
+local p9 = {}
+local raw = require "p9.raw"
+p9.raw = raw
+
+-- Defaults &c
+p9.iosize = 8192
+
+
+
+
+-- Utility / debugging
+
+local function fwrite(fmt, ...)
+	io.write(string.format(fmt, ...))
+end
+
+-- typecheck(values... : any, types : string) -> string...
+-- typecheck matches the types of values against a string
+-- of space-separated type names.
+-- If a value without a matching type is found an error
+-- describing the type mismatch is thrown.
+-- Otherwise the function returns a type of each value.
+local function typecheck(...)
+	local values = table.pack(...)
+	local t = table.remove(values)
+	local accept = {}
+	for w in t:gmatch("%w+") do
+		accept[w] = true
+	end
+	local types = {}
+	for _, v in ipairs(values) do
+		local tv = type(v)
+		types[#types + 1] = tv
+		if not accept[tv] then
+			error(("expected type(s) [%s], got %s"):format(t, tv), 2)
+		end
+	end
+	return table.unpack(types)
+end
+
+
+
+-- Namespace
+
+local mntmap <const> = {
+	["a"] = raw.MAFTER,
+	["b"] = raw.MBEFORE,
+	["c"] = raw.MCREATE,
+	["C"] = raw.MCACHE,
+}
+local function parsemntflags(s)
+	local f = raw.MREPL
+	for c in s:gmatch("%w") do
+		f = f | (mntmap[c] | 0)
+	end
+	return f
+end
+
+--- bind(string, string, [string]) -> int
+function p9.bind(name, over, flags)
+	return raw.bind(name, over, parsemntflags(flags or ""))
+end
+
+--- mount(int | string, int, string, [string, [string]]) -> int
+function p9.mount(fd, afd, over, flags, tree)
+	if type(fd) == string then
+		fd = p9.open(fd, "rw")
+	end
+	flags = parsemntflags(flags or "")
+	return raw.mount(fd, afd or -1, over, flags, tree or "")
+end
+
+--- unmount(string, [string]) -> int
+function p9.unmount(name, over)
+	if over == nil then
+		return raw.unmount(nil, name)
+	end
+	return raw.unmount(name, over)
+end
+
+
+
+
+-- Processes
+
+local rforkmap <const> = {
+	-- these are the same as in rc(1)
+	["n"] = raw.RFNAMEG,
+	["N"] = raw.RFCNAMEG,
+	["e"] = raw.RFENVG,
+	["E"] = raw.RFCENVG,
+	["s"] = raw.RFNOTEG,
+	["f"] = raw.RFFDG,
+	["F"] = raw.RFCFDG,
+	["m"] = raw.RFNOMNT,
+	-- these are new
+	["p"] = raw.RFPROC,
+	["&"] = raw.RFNOWAIT,
+	-- this is likely useless
+	["r"] = raw.RFREND,
+	-- this is gonna panic
+	["M"] = raw.RFMEM,
+}
+--- rfork(string) -> int
+function p9.rfork(flags)
+	local f = 0
+	for c in flags:gmatch("%w") do
+		f = f | (rforkmap[c] or 0)
+	end
+	return raw.rfork(f)
+end
+
+
+
+
+-- File I/O
+-- These are built on top of the raw API written in C and
+-- where applicable they provide a more polished interface
+-- with reasonable defaults.
+
+local modemap <const> = {
+	["r"] = raw.OREAD,
+	["w"] = raw.OWRITE,
+	["x"] = raw.OEXEC,
+	["T"] = raw.OTRUNC,
+	["C"] = raw.OCEXEC,
+	["R"] = raw.ORCLOSE,
+	["E"] = raw.OEXCL,
+}
+local function parsemode(s)
+	local m, o, c
+	
+	m = 0;
+	o = {r = false, w = false, x = false}
+	for c in s:gmatch("%w") do
+		if o[c] ~= nil then
+			o[c] = true
+		else
+			m = m | (modemap[c] or 0)
+		end
+	end
+	if o.r and o.w then
+		m = m | raw.ORDWR
+	elseif o.r then
+		m = m | raw.OREAD
+	elseif o.w then
+		m = m | raw.OWRITE
+	elseif o.x then
+		m = m | raw.OEXEC
+	end
+	return m
+end
+
+local permmap <const> = {
+	["d"] = raw.DMDIR,
+	["a"] = raw.DMAPPEND,
+	["e"] = raw.DMEXCL,
+	["t"] = raw.DMTMP,
+}
+local function parseperm(s)
+	local perm, m, p, c
+	
+	m, p = s:match("([daet]*)([0-7]*)")
+	perm = tonumber(p, 8) or 0644
+	for c in m:gmatch("%w") do
+		perm = perm | (permmap[c] or 0)
+	end
+	return perm
+end
+
+--- open(string, string) -> int
+function p9.open(file, ...)
+	local mode = ...
+	mode = parsemode(mode or "r")
+	return raw.open(file, mode)
+end
+
+--- create(string, [string, [string]]) -> int
+function p9.create(file, mode, perm)
+	if not mode or #mode == 0 then mode = "rw" end
+	if not perm or #perm == 0 then perm = "644" end
+	mode = parsemode(mode)
+	perm = parseperm(perm)
+	if perm & raw.DMDIR then
+		mode = mode & ~(raw.OWRITE)
+	end
+	return raw.create(file, mode, perm)
+end
+
+--- read(int, [int, [int]]) -> string
+function p9.read(fd, n, off)
+	return raw.read(fd, n or p9.iosize, off or -1)
+end
+
+--- slurp(int, [int]) -> string
+function p9.slurp(fd, max)
+	max = max or math.huge
+	local tot, n = 0, 0
+	local buf = {}
+	while true do
+		n = math.min(max - tot, p9.iosize)
+		local r = p9.read(fd, n)
+		if #r == 0 then
+			break
+		end
+		buf[#buf + 1] = r
+		tot = tot + #r
+		if tot == max then
+			break
+		end
+	end
+	return table.concat(buf)
+end
+
+--- write(int, string, [int, [int]]) -> int
+function p9.write(fd, buf, n, off)
+	return raw.write(fd, buf, n or #buf, off or -1)
+end
+
+local whencemap <const> = {
+	["set"] = 0,
+	["cur"] = 1,
+	["end"] = 2
+}
+--- seek(int, int, [string]) -> int
+function p9.seek(fd, n, whence)
+	whence = whence or "set"
+	if whencemap[whence] == nil then
+		error("whence must be one of [cur, set, end]")
+	end
+	return raw.seek(fd, n, whencemap[whence])
+end
+
+--- close(int) -> int
+function p9.close(fd)
+	return raw.close(fd)
+end
+
+--- remove(string) -> int
+function p9.remove(file)
+	return raw.remove(file)
+end
+
+--- fd2path(int) -> string
+function p9.fd2path(fd)
+	return raw.fd2path(fd)
+end
+
+--- pipe() -> int, int
+function p9.pipe()
+end
+
+
+
+-- The File object
+
+-- A file descriptor wrapper.
+
+-- p9.file(fd) takes an open file descriptor and returns a
+-- File object f which provides a convenient method interface
+-- to the usual file operations.
+-- p9.openfile and p9.createfile take a file name and open
+-- or create a file. They accept the same arguments as
+-- regular open and close.
+
+-- The file descriptor stored in f.fd is garbage collected,
+-- that is, it will be automatically closed once the File
+-- object becomes unreachable. Note how this means that f.fd
+-- should be used sparringly and with much care. In particular
+-- you shouldn't store it outside of f, since the actual file
+-- descriptor number might become invalid (closed) or refer
+-- to a completely different file after f is collected.
+
+-- The f.keep field can be set to true to prevent the finalizer
+-- from closing the file.
+
+local fileproto = {
+	fd = -1,
+	name = nil,
+	path = nil,
+	
+	read = function(self, ...) return p9.read(self.fd, ...) end,
+	write = function(self, ...) return p9.write(self.fd, ...) end,
+	slurp = function(self, ...) return p9.slurp(self.fd, ...) end,
+	seek = function(self, ...) return p9.seek(self.fd, ...) end,
+	close = function(self)
+		if self.fd == -1 then
+			return
+		end
+		p9.close(self.fd)
+		self.fd = -1
+	end,
+	
+	__close = function(f, _)
+		f:close()
+	end
+}
+
+--- file(int, [string]) -> file{}
+function p9.file(fd, name)
+	local f = {}
+	for k, v in pairs(fileproto) do
+		f[k] = v
+	end
+	f.fd = fd
+	f.name = name
+	f.path = p9.fd2path(fd)
+	return setmetatable(f, f)
+end
+
+-- openfile(string, ...) -> file{}
+function p9.openfile(file, ...)
+	return p9.file(p9.open(file, ...), file)
+end
+
+-- createfile(string, ...) -> file{}
+function p9.createfile(file, ...)
+	return p9.file(p9.create(file, ...), file)
+end
+
+
+
+
+-- Environment variables
+--
+-- p9.env object provides a map between the /env device and Lua,
+-- with its dynamic fields representing the environment variables.
+-- Assigning a value to the field writes to the environment:
+--
+-- 	p9.env.var = "value"
+--
+-- while reading a value reads from the environment:
+--
+-- 	assert(p9.env.var == "value")
+--
+-- A value can be a string or a list.
+-- A list is encoded (decoded) to (from) the environment as a
+-- list of strings according to the encoding used by the rc(1)
+-- shell (0-byte separated fields).
+--
+--	lua> p9.env.list = {"a", "b", "c"}
+--	rc> echo $#list -- $list
+--	3 -- a b c
+--
+-- p9.getenv(name) and p9.setenv(name, val) provide the more
+-- usual API.
+
+--- getenv(string) -> string | list
+function p9.getenv(key)
+	typecheck(key, "string")
+	
+	local f, err = io.open("/env/" .. key)
+	if err then
+		return nil, err
+	end
+	local buf = f:read("a")
+	f:close()
+	-- a value
+	if #buf == 0 or buf:sub(-1) ~= "\0" then
+		return buf
+	end
+	-- a list (as encoded by rc(1) shell)
+	local t, p = {}, 1
+	while p <= #buf do
+		t[#t + 1], p = string.unpack("z", buf, p)
+	end
+	return t
+end
+
+--- setenv(string, string | list)
+function p9.setenv(key, val)
+	local tk = typecheck(key, "string")
+	local tv = typecheck(val, "nil string table")
+	if val == nil then
+		pcall(p9.remove, "/env/" .. key)
+		return
+	end
+	local buf
+	if tv == "string" then
+		buf = val
+	elseif tv == "table" then
+		local t = {}
+		for i = 1, #val do
+			t[#t + 1] = string.pack("z", val[i])
+		end
+		buf = table.concat(t)
+	end
+	local f = assert(io.open("/env/" .. key, "w"))
+	if not f:write(buf) then
+		error("can't write environment")
+	end
+	f:close()
+end
+
+p9.env = setmetatable({}, {
+	__index = function(_, key)
+		return p9.getenv(key)
+	end,
+	__newindex = function(_, key, val)
+		p9.setenv(key, val)
+	end,
+})
+
+
+
+
+-- Lethal API
+-- p9.lethal() returns a proxy table that is just like the
+-- regular p9 module table, except that each function field
+-- is wrapped such that any error raised during a call
+-- results in program termination. An error is printed to
+-- fd 2 and the same error is used for the program's exit status.
+-- Note that this only mechanism only works for the fields
+-- of the p9 module, like the p9.open and friends.
+-- In particular, it doesn't apply to methods of File objects
+-- nor to the p9.env object.
+-- This limitation should be removed in the future.
+
+local lethalproxy = setmetatable({}, {
+	__index = function(_, key)
+		if type(p9[key]) ~= "function" then
+			return p9[key]
+		end
+		return function(...)
+			local res = table.pack(pcall(p9[key], ...))
+			if not res[1] then
+				p9.write(2, string.format("%s: %s\n", arg[0] or "lua", res[2]))
+				os.exit(res[2])
+			end
+			table.remove(res, 1)
+			return table.unpack(res)
+		end
+	end
+})
+
+--- lethal() -> p9{}
+function p9.lethal()
+	return lethalproxy
+end
+
+return p9
--- /dev/null
+++ b/ns.c
@@ -1,0 +1,46 @@
+static int
+p9_bind(lua_State *L)
+{
+	const char *this, *over;
+	lua_Integer flag;
+	int r;
+	
+	this = luaL_checkstring(L, 1);
+	over = luaL_checkstring(L, 2);
+	flag = luaL_checkinteger(L, 3);
+	if((r = bind(this, over, flag)) == -1)
+		lerror(L, "bind");
+	lua_pushinteger(L, r);
+	return 1;
+}
+
+static int
+p9_mount(lua_State *L)
+{
+	const char *over, *aname;
+	lua_Integer fd, afd, flag, r;
+	
+	fd = luaL_checkinteger(L, 1);
+	afd = luaL_checkinteger(L, 2);
+	over = luaL_checkstring(L, 3);
+	flag = luaL_checkinteger(L, 4);
+	aname = luaL_checkstring(L, 5);
+	if((r = mount(fd, afd, over, flag, aname)) == -1)
+		lerror(L, "mount");
+	lua_pushinteger(L, r);
+	return 1;
+}
+
+static int
+p9_unmount(lua_State *L)
+{
+	const char *name, *over;
+	int r;
+	
+	name = luaL_optstring(L, 1, nil);
+	over = luaL_checkstring(L, 2);
+	if((r = unmount(name, over)) == -1)
+		lerror(L, "unmount");
+	lua_pushinteger(L, r);
+	return 1;
+}
--- /dev/null
+++ b/p9.c
@@ -1,0 +1,165 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include <lua.h>
+#include <lauxlib.h>
+
+static void
+lerror(lua_State *L, char *call)
+{
+	char err[ERRMAX];
+	
+	rerrstr(err, sizeof err);
+	lua_pushfstring(L, "%s: %s", call, err);
+	lua_error(L);
+}
+
+/* Memory allocator associated with Lua state */
+static void*
+lalloc(lua_State *L, void *ptr, usize sz)
+{
+	void *ud;
+	
+	if((ptr = (lua_getallocf(L, &ud))(ud, ptr, LUA_TUSERDATA, sz)) == nil){
+		lua_pushliteral(L, "out of memory");
+		lua_error(L);
+	}
+	memset(ptr, 0, sz);
+	setmalloctag(ptr, getcallerpc(&L));
+	return ptr;
+}
+
+/* 
+ * Various functions in this library require a
+ * variably sized buffer for their operation.
+ * Rather than allocating one for each call
+ * we preallocate a shared buffer of reasonable
+ * size and grow it as needed.
+ * The buffer gets associated with a Lua state
+ * at library load time.
+ * getbuffer(L, sz) returns a pointer to the
+ * memory area of at least sz bytes.
+ * 
+ * To avoid stepping on each other's toes the
+ * buffer use must be constrained to a single
+ * call.
+ */
+
+typedef struct Buf {
+	usize sz;
+	char *b;
+} Buf;
+
+static Buf*
+resizebuffer(lua_State *L, Buf *buf, usize sz)
+{
+	if(buf == nil){
+		buf = lalloc(L, nil, sizeof(Buf));
+		buf->b = nil;
+		buf->sz = 0;
+	}
+	if(buf->sz < sz){
+		buf->b = lalloc(L, buf->b, sz);
+		buf->sz = sz;
+	}
+	return buf;
+}
+
+static char*
+getbuffer(lua_State *L, usize sz)
+{
+	Buf *buf;
+	
+	lua_getfield(L, LUA_REGISTRYINDEX, "p9-buffer");
+	buf = lua_touserdata(L, -1);
+	return resizebuffer(L, buf, sz)->b;
+}
+
+#include "fs.c"
+#include "ns.c"
+#include "proc.c"
+
+typedef struct Data {
+	char *key;
+	lua_Integer val;
+} Data;
+
+static Data p9data[] = {
+	{"OREAD", OREAD},
+	{"OWRITE", OWRITE},
+	{"ORDWR", ORDWR},
+	{"OEXEC", OEXEC},
+	{"OTRUNC", OTRUNC},
+	{"OCEXEC", OCEXEC},
+	{"ORCLOSE", ORCLOSE},
+	{"OEXCL", OEXCL},
+	
+	{"DMDIR", DMDIR},
+	{"DMAPPEND", DMAPPEND},
+	{"DMEXCL", DMEXCL},
+	{"DMMOUNT", DMMOUNT},
+	{"DMAUTH", DMAUTH},
+	{"DMTMP", DMTMP},
+	{"DMREAD", DMREAD},
+	{"DMWRITE", DMWRITE},
+	{"DMEXEC", DMEXEC},
+	
+	{"MREPL", MREPL},
+	{"MBEFORE", MBEFORE},
+	{"MAFTER", MAFTER},
+	{"MCREATE", MCREATE},
+	{"MCACHE", MCACHE},
+
+	{"RFPROC", RFPROC},
+	{"RFNOWAIT", RFNOWAIT},
+	{"RFNAMEG", RFNAMEG},
+	{"RFCNAMEG", RFCNAMEG},
+	{"RFNOMNT", RFNOMNT},
+	{"RFENVG", RFENVG},
+	{"RFCENVG", RFCENVG},
+	{"RFNOTEG", RFNOTEG},
+	{"RFFDG", RFFDG},
+	{"RFCFDG", RFCFDG},
+	{"RFREND", RFREND},
+	{"RFMEM", RFMEM},
+	
+	{nil, 0}
+};
+
+static luaL_Reg p9func[] = {
+	{"open", p9_open},
+	{"create", p9_create},
+	{"close", p9_close},
+	{"read", p9_read},
+	{"write", p9_write},
+	{"seek", p9_seek},
+	{"remove", p9_remove},
+	{"fd2path", p9_fd2path},
+	
+	{"bind", p9_bind},
+	{"mount", p9_mount},
+	{"unmount", p9_unmount},
+	
+	{"rfork", p9_rfork},
+	
+	{nil, nil}
+};
+
+int
+luaopen_p9(lua_State *L)
+{
+	Buf *buf;
+	Data *d;
+	
+	buf = resizebuffer(L, nil, 8192);
+	lua_pushlightuserdata(L, buf);
+	lua_setfield(L, LUA_REGISTRYINDEX, "p9-buffer");
+	
+	luaL_newlib(L, p9func);
+	for(d = p9data; d->key != nil; d++){
+		lua_pushinteger(L, d->val);
+		lua_setfield(L, -2, d->key);
+	}
+	return 1;
+}
--- /dev/null
+++ b/proc.c
@@ -1,0 +1,12 @@
+static int
+p9_rfork(lua_State *L)
+{
+	lua_Integer flags;
+	int r;
+	
+	flags = luaL_checkinteger(L, 1);
+	if((r = rfork(flags)) == -1)
+		lerror(L, "rfork");
+	lua_pushinteger(L, r);
+	return 1;
+}
--- /dev/null
+++ b/test.lua
@@ -1,0 +1,140 @@
+#!/bin/lua9
+
+local p9 = require "p9"
+
+local function tmp()
+	return string.format("/tmp/lua.%x", math.random(1e10))
+end
+
+p9.rfork("en")
+os.execute("ramfs")
+
+local t = {
+	{nil, nil}, {"", nil}, {nil, ""},
+	{"r", nil}, {"r", ""},
+	{"w", "d644"},
+	{"rw", "d644"},
+	{nil, "d644"},
+	{"", "d644"},
+	{"r", "d644"},
+	{"w", "d644"},
+	{"rw", "d644"}
+}
+
+for i = 1, #t do
+	local mode = t[i][1]
+	local perm = t[i][2]
+	p9.close(p9.create(tmp(), mode, perm))
+end
+
+
+
+-- File I/O
+do
+	local s = string.rep("ABCD", 2048*2) -- 16k > standard 8k buffer
+	local f = tmp()
+	local fd = p9.create(f, "rw")
+	p9.write(fd, s)
+	p9.close(fd)
+	fd = p9.open(f, "r")
+	assert(p9.slurp(fd) == s)
+	p9.close(fd)
+	fd = p9.open(f, "r")
+	assert(p9.slurp(fd, 2048) == string.rep("ABCD", 512))
+	p9.close(fd)
+	fd = p9.open(f, "r")
+	assert(p9.slurp(fd, 16*1024 + 999) == s)
+	p9.close(fd)
+	
+	fd = p9.open(f, "r")
+	assert(p9.seek(fd, 0, "end") == 16*1024)
+	assert(p9.seek(fd, 8192, "set") == 8192
+		and p9.slurp(fd) == string.rep("ABCD", 2*1024))
+	p9.seek(fd, 0)
+	assert(p9.seek(fd, 16*1024 - 4, "cur") == 16*1024 - 4
+		and p9.slurp(fd) == "ABCD")
+	p9.close(fd)
+end
+
+-- fd2path
+do
+	local fd = p9.create("/tmp/fd2path")
+	assert(p9.fd2path(fd) == "/tmp/fd2path")
+end
+
+-- File objects
+-- Closing
+-- Make sure it's closed
+local fd
+do
+	local f <close> = p9.createfile(tmp())
+	fd = f.fd
+end
+assert(pcall(p9.seek, fd, 0) == false)
+-- Make sure it's not closed
+local fd
+do
+	local f = p9.createfile(tmp())
+	fd = f.fd
+end
+assert(pcall(p9.seek, fd, 0) == true)
+p9.close(fd)
+
+-- Basic operations. These are the same as regular
+-- function calls so no need for much testing.
+do
+	local f <close> = p9.createfile(tmp(), "rw")
+	local data = string.rep("ABCD", 1024)
+	f:write(data)
+	f:seek(0)
+	assert(f:slurp() == data)
+end
+
+
+
+-- Namespaces
+-- bind and unmount work
+assert(pcall(function()
+	local f
+	p9.bind("#|", "/n/pipe")
+	f = p9.openfile("/n/pipe/data")
+	p9.unmount("/n/pipe")
+	assert(pcall(p9.openfile, "/n/pipe/data") == false)
+end))
+-- mount works
+assert(pcall(function()
+	assert(p9.mount(p9.open("/srv/cwfs", "rw"), nil, "/n/test"))
+	assert(p9.openfile("/n/test/lib/glass"))
+end))
+
+
+-- Process control
+-- No idea how to test this properly.
+
+
+
+-- Environment variables
+do
+	local e
+	
+	assert(p9.env["sure-is-empty"] == nil)
+	-- Set and get a string variable
+	p9.env.test = "ABC"; assert(p9.env.test == "ABC")
+	-- Delete a variable
+	p9.env.test = nil; assert(p9.env.test == nil)
+	
+	-- Set and get a list variable
+	p9.env.test = {"a", "b", "c"}
+	e = p9.env.test
+	assert(type(e) == "table"
+		and #e == 3 and e[1] == "a" and e[2] == "b" and e[3] == "c")
+	-- Ensure it's understood by rc
+	os.execute("echo -n $#test $test >/env/res")
+	assert(p9.env.res == "3 a b c")
+	-- Ensure we understand rc
+	os.execute("test=(d e f)")
+	e = p9.env.test
+	assert(type(e) == "table"
+		and #e == 3 and e[1] == "d" and e[2] == "e" and e[3] == "f")
+	p9.env.test = nil
+end