Reading files!

- File.close()
- File.open()
- File.read()
- file.readBytes()

And a few other little methods. Still lots more work to do, but it's a
start.
This commit is contained in:
Bob Nystrom
2015-09-30 21:13:36 -07:00
parent bacbd85543
commit b7ed774da3
32 changed files with 475 additions and 44 deletions

View File

@ -7,7 +7,14 @@
#include "scheduler.wren.inc"
#include "timer.wren.inc"
extern void fileStartSize(WrenVM* vm);
extern void fileAllocate(WrenVM* vm);
extern void fileFinalize(WrenVM* vm);
extern void fileOpen(WrenVM* vm);
extern void fileSizePath(WrenVM* vm);
extern void fileClose(WrenVM* vm);
extern void fileDescriptor(WrenVM* vm);
extern void fileReadBytes(WrenVM* vm);
extern void fileSize(WrenVM* vm);
extern void schedulerCaptureMethods(WrenVM* vm);
extern void timerStartTimer(WrenVM* vm);
@ -19,7 +26,7 @@ extern void timerStartTimer(WrenVM* vm);
// If you add a new method to the longest class below, make sure to bump this.
// Note that it also includes an extra slot for the sentinel value indicating
// the end of the list.
#define MAX_METHODS_PER_CLASS 2
#define MAX_METHODS_PER_CLASS 9
// The maximum number of foreign classes a single built-in module defines.
//
@ -75,7 +82,14 @@ static ModuleRegistry modules[] =
{
MODULE(io)
CLASS(File)
STATIC_METHOD("startSize_(_,_)", fileStartSize)
STATIC_METHOD("<allocate>", fileAllocate)
STATIC_METHOD("<finalize>", fileFinalize)
STATIC_METHOD("open_(_,_)", fileOpen)
STATIC_METHOD("sizePath_(_,_)", fileSizePath)
METHOD("close_(_)", fileClose)
METHOD("descriptor", fileDescriptor)
METHOD("readBytes_(_,_)", fileReadBytes)
METHOD("size_(_)", fileSize)
END_CLASS
END_MODULE

View File

@ -18,6 +18,9 @@ static uv_loop_t* loop;
static char const* rootDirectory = NULL;
// The exit code to use unless some other error overrides it.
int defaultExitCode = 0;
// Reads the contents of the file at [path] and returns it as a heap allocated
// string.
//
@ -220,6 +223,8 @@ void runFile(const char* path)
// Exit with an error code if the script failed.
if (result == WREN_RESULT_COMPILE_ERROR) exit(65); // EX_DATAERR.
if (result == WREN_RESULT_RUNTIME_ERROR) exit(70); // EX_SOFTWARE.
if (defaultExitCode != 0) exit(defaultExitCode);
}
int runRepl()
@ -262,6 +267,11 @@ uv_loop_t* getLoop()
return loop;
}
void setExitCode(int exitCode)
{
defaultExitCode = exitCode;
}
void setTestCallbacks(WrenBindForeignMethodFn bindMethod,
WrenBindForeignClassFn bindClass,
WrenForeignMethodFn afterLoad)

View File

@ -18,6 +18,9 @@ WrenVM* getVM();
// Gets the event loop the VM is using.
uv_loop_t* getLoop();
// Set the exit code the CLI should exit with when done.
void setExitCode(int exitCode);
// Adds additional callbacks to use when binding foreign members from Wren.
//
// Used by the API test executable to let it wire up its own foreign functions.

View File

@ -9,32 +9,171 @@
#include <stdio.h>
// Called by libuv when the stat call for size completes.
static void sizeCallback(uv_fs_t* request)
void fileAllocate(WrenVM* vm)
{
// Store the file descriptor in the foreign data, so that we can get to it
// in the finalizer.
int* fd = (int*)wrenAllocateForeign(vm, sizeof(int));
*fd = (int)wrenGetArgumentDouble(vm, 1);
}
void fileFinalize(WrenVM* vm)
{
int fd = *(int*)wrenGetArgumentForeign(vm, 0);
// Already closed.
if (fd == -1) return;
uv_fs_t request;
uv_fs_close(getLoop(), &request, fd, NULL);
uv_fs_req_cleanup(&request);
}
// If [request] failed with an error, sends the runtime error to the VM and
// frees the request.
//
// Returns true if an error was reported.
static bool handleRequestError(uv_fs_t* request)
{
if (request->result >= 0) return false;
WrenValue* fiber = (WrenValue*)request->data;
schedulerResumeError(fiber, uv_strerror((int)request->result));
uv_fs_req_cleanup(request);
free(request);
return true;
}
// Allocates a new request that resumes [fiber] when it completes.
uv_fs_t* createRequest(WrenValue* fiber)
{
uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t));
request->data = fiber;
return request;
}
// Releases resources used by [request].
//
// Returns the fiber that should be resumed after [request] completes.
WrenValue* freeRequest(uv_fs_t* request)
{
WrenValue* fiber = (WrenValue*)request->data;
if (request->result != 0)
{
schedulerResumeString(fiber, uv_strerror((int)request->result));
uv_fs_req_cleanup(request);
return;
}
uv_fs_req_cleanup(request);
free(request);
return fiber;
}
static void openCallback(uv_fs_t* request)
{
if (handleRequestError(request)) return;
double fd = (double)request->result;
WrenValue* fiber = freeRequest(request);
schedulerResumeDouble(fiber, fd);
}
void fileOpen(WrenVM* vm)
{
const char* path = wrenGetArgumentString(vm, 1);
uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2));
// TODO: Allow controlling flags and modes.
uv_fs_open(getLoop(), request, path, O_RDONLY, S_IRUSR, openCallback);
}
// Called by libuv when the stat call for size completes.
static void sizeCallback(uv_fs_t* request)
{
if (handleRequestError(request)) return;
double size = (double)request->statbuf.st_size;
uv_fs_req_cleanup(request);
WrenValue* fiber = freeRequest(request);
schedulerResumeDouble(fiber, size);
}
void fileStartSize(WrenVM* vm)
void fileSizePath(WrenVM* vm)
{
const char* path = wrenGetArgumentString(vm, 1);
WrenValue* fiber = wrenGetArgumentValue(vm, 2);
// Store the fiber to resume when the request completes.
uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t));
request->data = fiber;
uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2));
uv_fs_stat(getLoop(), request, path, sizeCallback);
}
static void closeCallback(uv_fs_t* request)
{
if (handleRequestError(request)) return;
WrenValue* fiber = freeRequest(request);
schedulerResume(fiber);
}
void fileClose(WrenVM* vm)
{
int* foreign = (int*)wrenGetArgumentForeign(vm, 0);
int fd = *foreign;
// If it's already closed, we're done.
if (fd == -1)
{
wrenReturnBool(vm, true);
return;
}
// Mark it closed immediately.
*foreign = -1;
uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 1));
uv_fs_close(getLoop(), request, fd, closeCallback);
wrenReturnBool(vm, false);
}
void fileDescriptor(WrenVM* vm)
{
int* foreign = (int*)wrenGetArgumentForeign(vm, 0);
int fd = *foreign;
wrenReturnDouble(vm, fd);
}
static void readBytesCallback(uv_fs_t* request)
{
if (handleRequestError(request)) return;
uv_buf_t buffer = request->bufs[0];
WrenValue* fiber = freeRequest(request);
// TODO: Having to copy the bytes here is a drag. It would be good if Wren's
// embedding API supported a way to *give* it bytes that were previously
// allocated using Wren's own allocator.
schedulerResumeBytes(fiber, buffer.base, buffer.len);
// TODO: Likewise, freeing this after we resume is lame.
free(buffer.base);
}
void fileReadBytes(WrenVM* vm)
{
uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2));
int fd = *(int*)wrenGetArgumentForeign(vm, 0);
// TODO: Assert fd != -1.
uv_buf_t buffer;
buffer.len = (size_t)wrenGetArgumentDouble(vm, 1);
buffer.base = (char*)malloc(buffer.len);
// TODO: Allow passing in offset.
uv_fs_read(getLoop(), request, fd, &buffer, 1, 0, readBytesCallback);
}
void fileSize(WrenVM* vm)
{
uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 1));
int fd = *(int*)wrenGetArgumentForeign(vm, 0);
// TODO: Assert fd != -1.
uv_fs_fstat(getLoop(), request, fd, sizeCallback);
}

View File

@ -1,15 +1,69 @@
import "scheduler" for Scheduler
class File {
static size(path) {
foreign class File {
static open(path) {
if (!(path is String)) Fiber.abort("Path must be a string.")
startSize_(path, Fiber.current)
var result = Scheduler.runNextScheduled_()
if (result is String) Fiber.abort(result)
open_(path, Fiber.current)
var fd = Scheduler.runNextScheduled_()
return new_(fd)
}
static open(path, fn) {
var file = open(path)
var fiber = Fiber.new { fn.call(file) }
// Poor man's finally. Can we make this more elegant?
var result = fiber.try()
file.close()
// TODO: Want something like rethrow since now the callstack ends here. :(
if (fiber.error != null) Fiber.abort(fiber.error)
return result
}
foreign static startSize_(path, fiber)
static read(path) {
return File.open(path) {|file| file.readBytes(file.size) }
}
static size(path) {
if (!(path is String)) Fiber.abort("Path must be a string.")
sizePath_(path, Fiber.current)
return Scheduler.runNextScheduled_()
}
construct new_(fd) {}
close() {
if (close_(Fiber.current)) return
Scheduler.runNextScheduled_()
}
isOpen { descriptor != -1 }
size {
if (!isOpen) Fiber.abort("File is not open.")
size_(Fiber.current)
return Scheduler.runNextScheduled_()
}
readBytes(count) {
if (!isOpen) Fiber.abort("File is not open.")
if (!(count is Num)) Fiber.abort("Count must be an integer.")
if (!count.isInteger) Fiber.abort("Count must be an integer.")
if (count < 0) Fiber.abort("Count cannot be negative.")
readBytes_(count, Fiber.current)
return Scheduler.runNextScheduled_()
}
foreign static open_(path, fiber)
foreign static sizePath_(path, fiber)
foreign close_(fiber)
foreign descriptor
foreign readBytes_(count, fiber)
foreign size_(fiber)
}

View File

@ -2,16 +2,70 @@
static const char* ioModuleSource =
"import \"scheduler\" for Scheduler\n"
"\n"
"class File {\n"
" static size(path) {\n"
"foreign class File {\n"
" static open(path) {\n"
" if (!(path is String)) Fiber.abort(\"Path must be a string.\")\n"
"\n"
" startSize_(path, Fiber.current)\n"
" var result = Scheduler.runNextScheduled_()\n"
" if (result is String) Fiber.abort(result)\n"
" open_(path, Fiber.current)\n"
" var fd = Scheduler.runNextScheduled_()\n"
" return new_(fd)\n"
" }\n"
"\n"
" static open(path, fn) {\n"
" var file = open(path)\n"
" var fiber = Fiber.new { fn.call(file) }\n"
"\n"
" // Poor man's finally. Can we make this more elegant?\n"
" var result = fiber.try()\n"
" file.close()\n"
"\n"
" // TODO: Want something like rethrow since now the callstack ends here. :(\n"
" if (fiber.error != null) Fiber.abort(fiber.error)\n"
" return result\n"
" }\n"
"\n"
" foreign static startSize_(path, fiber)\n"
" static read(path) {\n"
" return File.open(path) {|file| file.readBytes(file.size) }\n"
" }\n"
"\n"
" static size(path) {\n"
" if (!(path is String)) Fiber.abort(\"Path must be a string.\")\n"
"\n"
" sizePath_(path, Fiber.current)\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
"\n"
" construct new_(fd) {}\n"
"\n"
" close() {\n"
" if (close_(Fiber.current)) return\n"
" Scheduler.runNextScheduled_()\n"
" }\n"
"\n"
" isOpen { descriptor != -1 }\n"
"\n"
" size {\n"
" if (!isOpen) Fiber.abort(\"File is not open.\")\n"
"\n"
" size_(Fiber.current)\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
"\n"
" readBytes(count) {\n"
" if (!isOpen) Fiber.abort(\"File is not open.\")\n"
" if (!(count is Num)) Fiber.abort(\"Count must be an integer.\")\n"
" if (!count.isInteger) Fiber.abort(\"Count must be an integer.\")\n"
" if (count < 0) Fiber.abort(\"Count cannot be negative.\")\n"
"\n"
" readBytes_(count, Fiber.current)\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
"\n"
" foreign static open_(path, fiber)\n"
" foreign static sizePath_(path, fiber)\n"
"\n"
" foreign close_(fiber)\n"
" foreign descriptor\n"
" foreign readBytes_(count, fiber)\n"
" foreign size_(fiber)\n"
"}\n";

View File

@ -12,33 +12,63 @@
// one.
static WrenValue* resume;
static WrenValue* resumeWithArg;
static WrenValue* resumeError;
void schedulerCaptureMethods(WrenVM* vm)
{
resume = wrenGetMethod(vm, "scheduler", "Scheduler", "resume_(_)");
resumeWithArg = wrenGetMethod(vm, "scheduler", "Scheduler", "resume_(_,_)");
resumeError = wrenGetMethod(vm, "scheduler", "Scheduler", "resumeError_(_,_)");
}
static void callResume(WrenValue* resumeMethod, WrenValue* fiber,
const char* argTypes, ...)
{
va_list args;
va_start(args, argTypes);
WrenInterpretResult result = wrenCallVarArgs(getVM(), resumeMethod, NULL,
argTypes, args);
va_end(args);
wrenReleaseValue(getVM(), fiber);
// If a runtime error occurs in response to an async operation and nothing
// catches the error in the fiber, then exit the CLI.
if (result == WREN_RESULT_RUNTIME_ERROR)
{
uv_stop(getLoop());
setExitCode(70); // EX_SOFTWARE.
}
}
void schedulerResume(WrenValue* fiber)
{
wrenCall(getVM(), resume, NULL, "v", fiber);
wrenReleaseValue(getVM(), fiber);
callResume(resume, fiber, "v", fiber);
}
void schedulerResumeBytes(WrenValue* fiber, const char* bytes, size_t length)
{
callResume(resumeWithArg, fiber, "va", fiber, bytes, length);
}
void schedulerResumeDouble(WrenValue* fiber, double value)
{
wrenCall(getVM(), resumeWithArg, NULL, "vd", fiber, value);
wrenReleaseValue(getVM(), fiber);
callResume(resumeWithArg, fiber, "vd", fiber, value);
}
void schedulerResumeString(WrenValue* fiber, const char* text)
{
wrenCall(getVM(), resumeWithArg, NULL, "vs", fiber, text);
wrenReleaseValue(getVM(), fiber);
callResume(resumeWithArg, fiber, "vs", fiber, text);
}
void schedulerResumeError(WrenValue* fiber, const char* error)
{
callResume(resumeError, fiber, "vs", fiber, error);
}
void schedulerReleaseMethods()
{
if (resume != NULL) wrenReleaseValue(getVM(), resume);
if (resumeWithArg != NULL) wrenReleaseValue(getVM(), resumeWithArg);
if (resumeError != NULL) wrenReleaseValue(getVM(), resumeError);
}

View File

@ -4,8 +4,10 @@
#include "wren.h"
void schedulerResume(WrenValue* fiber);
void schedulerResumeBytes(WrenValue* fiber, const char* bytes, size_t length);
void schedulerResumeDouble(WrenValue* fiber, double value);
void schedulerResumeString(WrenValue* fiber, const char* text);
void schedulerResumeError(WrenValue* fiber, const char* error);
void schedulerReleaseMethods();

View File

@ -11,6 +11,7 @@ class Scheduler {
// Called by native code.
static resume_(fiber) { fiber.transfer() }
static resume_(fiber, arg) { fiber.transfer(arg) }
static resumeError_(fiber, error) { fiber.transferError(error) }
static runNextScheduled_() {
if (__scheduled == null || __scheduled.isEmpty) {

View File

@ -13,6 +13,7 @@ static const char* schedulerModuleSource =
" // Called by native code.\n"
" static resume_(fiber) { fiber.transfer() }\n"
" static resume_(fiber, arg) { fiber.transfer(arg) }\n"
" static resumeError_(fiber, error) { fiber.transferError(error) }\n"
"\n"
" static runNextScheduled_() {\n"
" if (__scheduled == null || __scheduled.isEmpty) {\n"

19
test/io/file/close.wren Normal file
View File

@ -0,0 +1,19 @@
import "io" for File
import "scheduler" for Scheduler
// See also: is_open.wren.
var file = File.open("test/io/file/close.wren")
System.print(file.close()) // expect: null
// Can call multiple times.
file.close()
// If already closed, returns synchronously.
Scheduler.add {
System.print("does not print")
}
file.close()
System.print("sync") // expect: sync

View File

@ -0,0 +1,11 @@
import "io" for File
var file = File.open("test/io/file/descriptor.wren")
// We can't test for a specific value since it's up to the OS, but it should be
// a positive number.
System.print(file.descriptor is Num) // expect: true
System.print(file.descriptor > 0) // expect: true
file.close()
System.print(file.descriptor) // expect: -1

1
test/io/file/file.txt Normal file
View File

@ -0,0 +1 @@
this is a text file

View File

@ -0,0 +1,6 @@
import "io" for File
var file = File.open("test/io/file/is_open.wren")
System.print(file.isOpen) // expect: true
file.close()
System.print(file.isOpen) // expect: false

5
test/io/file/open.wren Normal file
View File

@ -0,0 +1,5 @@
import "io" for File
var file = File.open("test/io/file/open.wren")
System.print(file is File) // expect: true
System.print(file.isOpen) // expect: true

View File

@ -0,0 +1,16 @@
import "io" for File
var stash
File.open("test/io/file/open_block.wren") {|file|
System.print(file is File) // expect: true
System.print(file.isOpen) // expect: true
stash = file
}
// Closes after block.
System.print(stash.isOpen) // expect: false
// Returns null.
System.print(File.open("test/io/file/open_block.wren") {|file|}) // expect: null
// TODO: Test a fiber aborting inside the block.

View File

@ -0,0 +1,3 @@
import "io" for File
File.open("nonexistent") {|file|} // expect runtime error: no such file or directory

View File

@ -0,0 +1,4 @@
import "io" for File
File.open("test/io/file/open_block_wrong_block_type.wren",
"no callable") // expect runtime error: String does not implement 'call(_)'.

View File

@ -0,0 +1,3 @@
import "io" for File
File.open(123) {|file|} // expect runtime error: Path must be a string.

View File

@ -0,0 +1,3 @@
import "io" for File
File.open("nonexistent") // expect runtime error: no such file or directory

View File

@ -0,0 +1,3 @@
import "io" for File
File.open(123) // expect runtime error: Path must be a string.

8
test/io/file/read.wren Normal file
View File

@ -0,0 +1,8 @@
import "io" for File
var text = File.read("test/io/file/file.txt")
System.print(text) // expect: this is a text file
System.print(text.count) // expect: 19
// TODO: A file containing line endings.
// TODO: A file containing null bytes.

View File

@ -0,0 +1,13 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
System.print(file.readBytes(3)) // expect: thi
// Always reads from the beginning.
System.print(file.readBytes(7)) // expect: this is
// Allows zero.
System.print(file.readBytes(0).bytes.count) // expect: 0
file.close()

View File

@ -0,0 +1,6 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
file.close()
file.readBytes(3) // expect runtime error: File is not open.

View File

@ -0,0 +1,4 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
file.readBytes("not num") // expect runtime error: Count must be an integer.

View File

@ -0,0 +1,4 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
file.readBytes(-1) // expect runtime error: Count cannot be negative.

View File

@ -0,0 +1,4 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
file.readBytes(1.2) // expect runtime error: Count must be an integer.

View File

@ -0,0 +1,3 @@
import "io" for File
File.read("nonexistent") // expect runtime error: no such file or directory

View File

@ -0,0 +1,3 @@
import "io" for File
File.read(123) // expect runtime error: Path must be a string.

View File

@ -1,7 +1,7 @@
import "io" for File
import "scheduler" for Scheduler
System.print(File.size("test/io/file/size.wren")) // expect: 401
System.print(File.size("test/io/file/size.wren")) // expect: 270
// Runs asynchronously.
Scheduler.add {
@ -10,9 +10,4 @@ Scheduler.add {
System.print(File.size("test/io/file/size.wren"))
// expect: async
// expect: 401
var error = Fiber.new {
System.print(File.size("nonexistent"))
}.try()
System.print(error) // expect: no such file or directory
// expect: 270

View File

@ -0,0 +1,6 @@
import "io" for File
var file = File.open("test/io/file/file.txt")
file.close()
file.size // expect runtime error: File is not open.

View File

@ -0,0 +1,3 @@
import "io" for File
File.size("nonexistent") // expect runtime error: no such file or directory