From d431c2eaa86bb49a7bb5395abafb80d707ec9321 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Fri, 16 Oct 2015 21:05:24 -0700 Subject: [PATCH] Start sketching in support for reading from stdin. --- src/cli/modules.c | 18 +++-- src/cli/vm.c | 9 ++- src/module/io.c | 99 ++++++++++++++++++++++- src/module/io.h | 11 +++ src/module/io.wren | 55 +++++++++++++ src/module/io.wren.inc | 55 +++++++++++++ src/module/scheduler.c | 2 +- src/module/scheduler.h | 2 +- test/io/stdin/aborts_on_eof.wren | 9 +++ test/io/stdin/read_line.wren | 11 +++ util/test.py | 6 +- util/xcode/wren.xcodeproj/project.pbxproj | 2 + 12 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 src/module/io.h create mode 100644 test/io/stdin/aborts_on_eof.wren create mode 100644 test/io/stdin/read_line.wren diff --git a/src/cli/modules.c b/src/cli/modules.c index c0cedfdb..3fbc49ce 100644 --- a/src/cli/modules.c +++ b/src/cli/modules.c @@ -15,6 +15,8 @@ extern void fileClose(WrenVM* vm); extern void fileDescriptor(WrenVM* vm); extern void fileReadBytes(WrenVM* vm); extern void fileSize(WrenVM* vm); +extern void stdinReadStart(WrenVM* vm); +extern void stdinReadStop(WrenVM* vm); extern void schedulerCaptureMethods(WrenVM* vm); extern void timerStartTimer(WrenVM* vm); @@ -33,7 +35,7 @@ extern void timerStartTimer(WrenVM* vm); // If you add a new class to the largest module 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_CLASSES_PER_MODULE 2 +#define MAX_CLASSES_PER_MODULE 3 // Describes one foreign method in a class. typedef struct @@ -68,11 +70,11 @@ typedef struct // To locate foreign classes and modules, we build a big directory for them in // static data. The nested collection initializer syntax gets pretty noisy, so // define a couple of macros to make it easier. -#define MODULE(name) { #name, &name##ModuleSource, { { -#define END_MODULE }, {NULL, {}} } }, +#define MODULE(name) { #name, &name##ModuleSource, { +#define END_MODULE {NULL, {}} }, }, -#define CLASS(name) #name, { -#define END_CLASS {false, NULL, NULL} } +#define CLASS(name) { #name, { +#define END_CLASS {false, NULL, NULL} }, }, #define METHOD(signature, fn) {false, signature, fn}, #define STATIC_METHOD(signature, fn) {true, signature, fn}, @@ -91,14 +93,16 @@ static ModuleRegistry modules[] = METHOD("readBytes_(_,_)", fileReadBytes) METHOD("size_(_)", fileSize) END_CLASS + CLASS(Stdin) + STATIC_METHOD("readStart_()", stdinReadStart) + STATIC_METHOD("readStop_()", stdinReadStop) + END_CLASS END_MODULE - MODULE(scheduler) CLASS(Scheduler) STATIC_METHOD("captureMethods_()", schedulerCaptureMethods) END_CLASS END_MODULE - MODULE(timer) CLASS(Timer) STATIC_METHOD("startTimer_(_,_)", timerStartTimer) diff --git a/src/cli/vm.c b/src/cli/vm.c index 0568566f..51b6f083 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -1,6 +1,7 @@ #include #include +#include "io.h" #include "modules.h" #include "scheduler.h" #include "vm.h" @@ -152,6 +153,7 @@ static WrenForeignClassMethods bindForeignClass( static void write(WrenVM* vm, const char* text) { printf("%s", text); + fflush(stdout); } static void initVM() @@ -175,12 +177,15 @@ static void initVM() static void freeVM() { - schedulerReleaseMethods(); + ioShutdown(); + schedulerShutdown(); uv_loop_close(loop); free(loop); - + wrenFreeVM(vm); + + uv_tty_reset_mode(); } void runFile(const char* path) diff --git a/src/module/io.c b/src/module/io.c index 88c9613a..48779423 100644 --- a/src/module/io.c +++ b/src/module/io.c @@ -9,6 +9,36 @@ #include +static const int stdinDescriptor = 0; + +// Handle to Stdin.onData_(). Called when libuv provides data on stdin. +static WrenValue* stdinOnData = NULL; + +// The stream used to read from stdin. Initialized on the first read. +static uv_stream_t* stdinStream = NULL; + +// Frees all resources related to stdin. +static void shutdownStdin() +{ + if (stdinStream != NULL) + { + uv_close((uv_handle_t*)stdinStream, NULL); + free(stdinStream); + stdinStream = NULL; + } + + if (stdinOnData != NULL) + { + wrenReleaseValue(getVM(), stdinOnData); + stdinOnData = NULL; + } +} + +void ioShutdown() +{ + shutdownStdin(); +} + void fileAllocate(WrenVM* vm) { // Store the file descriptor in the foreign data, so that we can get to it @@ -137,7 +167,7 @@ void fileDescriptor(WrenVM* vm) wrenReturnDouble(vm, fd); } -static void readBytesCallback(uv_fs_t* request) +static void fileReadBytesCallback(uv_fs_t* request) { if (handleRequestError(request)) return; @@ -165,7 +195,7 @@ void fileReadBytes(WrenVM* vm) buffer.base = (char*)malloc(buffer.len); // TODO: Allow passing in offset. - uv_fs_read(getLoop(), request, fd, &buffer, 1, 0, readBytesCallback); + uv_fs_read(getLoop(), request, fd, &buffer, 1, 0, fileReadBytesCallback); } void fileSize(WrenVM* vm) @@ -177,3 +207,68 @@ void fileSize(WrenVM* vm) uv_fs_fstat(getLoop(), request, fd, sizeCallback); } + +static void allocCallback(uv_handle_t* handle, size_t suggestedSize, + uv_buf_t* buf) +{ + // TODO: Handle allocation failure. + buf->base = malloc(suggestedSize); + buf->len = suggestedSize; +} + +static void stdinReadCallback(uv_stream_t* stream, ssize_t numRead, + const uv_buf_t* buffer) +{ + // If stdin was closed, send null to let io.wren know. + if (numRead == UV_EOF) + { + wrenCall(getVM(), stdinOnData, NULL, "v", NULL); + shutdownStdin(); + return; + } + + // TODO: Handle other errors. + + if (stdinOnData == NULL) + { + stdinOnData = wrenGetMethod(getVM(), "io", "Stdin", "onData_(_)"); + } + + // 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. + wrenCall(getVM(), stdinOnData, NULL, "a", buffer->base, numRead); + + // TODO: Likewise, freeing this after we resume is lame. + free(buffer->base); +} + +void stdinReadStart(WrenVM* vm) +{ + if (stdinStream == NULL) + { + if (uv_guess_handle(stdinDescriptor) == UV_TTY) + { + // stdin is connected to a terminal. + uv_tty_t* handle = (uv_tty_t*)malloc(sizeof(uv_tty_t)); + uv_tty_init(getLoop(), handle, stdinDescriptor, true); + stdinStream = (uv_stream_t*)handle; + } + else + { + // stdin is a pipe or a file. + uv_pipe_t* handle = (uv_pipe_t*)malloc(sizeof(uv_pipe_t)); + uv_pipe_init(getLoop(), handle, false); + uv_pipe_open(handle, stdinDescriptor); + stdinStream = (uv_stream_t*)handle; + } + } + + uv_read_start(stdinStream, allocCallback, stdinReadCallback); + // TODO: Check return. +} + +void stdinReadStop(WrenVM* vm) +{ + uv_read_stop(stdinStream); +} diff --git a/src/module/io.h b/src/module/io.h new file mode 100644 index 00000000..7d847869 --- /dev/null +++ b/src/module/io.h @@ -0,0 +1,11 @@ +#ifndef io_h +#define io_h + +#include "wren.h" + +// Frees up any pending resources in use by the IO module. +// +// In particular, this closes down the stdin stream. +void ioShutdown(); + +#endif diff --git a/src/module/io.wren b/src/module/io.wren index 6320ce7b..03ca9ce0 100644 --- a/src/module/io.wren +++ b/src/module/io.wren @@ -67,3 +67,58 @@ foreign class File { foreign readBytes_(count, fiber) foreign size_(fiber) } + +class Stdin { + static readLine() { + if (__isClosed == true) { + Fiber.abort("Stdin was closed.") + } + + // TODO: Error if other fiber is already waiting. + readStart_() + + __waitingFiber = Fiber.current + var line = Scheduler.runNextScheduled_() + + readStop_() + return line + } + + static onData_(data) { + if (data == null) { + __isClosed = true + readStop_() + + if (__line != null) { + var line = __line + __line = null + if (__waitingFiber != null) __waitingFiber.transfer(line) + } else { + __waitingFiber.transferError("Stdin was closed.") + } + } + + // TODO: Handle Windows line separators. + var lineSeparator = data.indexOf("\n") + + if (__line == null) __line = "" + if (lineSeparator == -1) { + // No end of line yet, so just accumulate it. + __line = __line + data + } else { + // Split the line at the separator. + var line = __line + data[0...lineSeparator] + if (lineSeparator > 0 && lineSeparator < data.count - 1) { + // Buffer up the characters after the separator for the next line. + __line = data[lineSeparator + 1..-1] + } else { + __line = "" + } + + if (__waitingFiber != null) __waitingFiber.transfer(line) + } + } + + foreign static readStart_() + foreign static readStop_() +} diff --git a/src/module/io.wren.inc b/src/module/io.wren.inc index 4cbfa968..b9b5272f 100644 --- a/src/module/io.wren.inc +++ b/src/module/io.wren.inc @@ -68,4 +68,59 @@ static const char* ioModuleSource = " foreign descriptor\n" " foreign readBytes_(count, fiber)\n" " foreign size_(fiber)\n" +"}\n" +"\n" +"class Stdin {\n" +" static readLine() {\n" +" if (__isClosed == true) {\n" +" Fiber.abort(\"Stdin was closed.\")\n" +" }\n" +"\n" +" // TODO: Error if other fiber is already waiting.\n" +" readStart_()\n" +"\n" +" __waitingFiber = Fiber.current\n" +" var line = Scheduler.runNextScheduled_()\n" +"\n" +" readStop_()\n" +" return line\n" +" }\n" +"\n" +" static onData_(data) {\n" +" if (data == null) {\n" +" __isClosed = true\n" +" readStop_()\n" +"\n" +" if (__line != null) {\n" +" var line = __line\n" +" __line = null\n" +" if (__waitingFiber != null) __waitingFiber.transfer(line)\n" +" } else {\n" +" __waitingFiber.transferError(\"Stdin was closed.\")\n" +" }\n" +" }\n" +"\n" +" // TODO: Handle Windows line separators.\n" +" var lineSeparator = data.indexOf(\"\n\")\n" +"\n" +" if (__line == null) __line = \"\"\n" +" if (lineSeparator == -1) {\n" +" // No end of line yet, so just accumulate it.\n" +" __line = __line + data\n" +" } else {\n" +" // Split the line at the separator.\n" +" var line = __line + data[0...lineSeparator]\n" +" if (lineSeparator > 0 && lineSeparator < data.count - 1) {\n" +" // Buffer up the characters after the separator for the next line.\n" +" __line = data[lineSeparator + 1..-1]\n" +" } else {\n" +" __line = \"\"\n" +" }\n" +"\n" +" if (__waitingFiber != null) __waitingFiber.transfer(line)\n" +" }\n" +" }\n" +"\n" +" foreign static readStart_()\n" +" foreign static readStop_()\n" "}\n"; diff --git a/src/module/scheduler.c b/src/module/scheduler.c index 7060fbb3..20347eb8 100644 --- a/src/module/scheduler.c +++ b/src/module/scheduler.c @@ -66,7 +66,7 @@ void schedulerResumeError(WrenValue* fiber, const char* error) callResume(resumeError, fiber, "vs", fiber, error); } -void schedulerReleaseMethods() +void schedulerShutdown() { if (resume != NULL) wrenReleaseValue(getVM(), resume); if (resumeWithArg != NULL) wrenReleaseValue(getVM(), resumeWithArg); diff --git a/src/module/scheduler.h b/src/module/scheduler.h index 3bec2be8..e90ce1e4 100644 --- a/src/module/scheduler.h +++ b/src/module/scheduler.h @@ -9,6 +9,6 @@ void schedulerResumeDouble(WrenValue* fiber, double value); void schedulerResumeString(WrenValue* fiber, const char* text); void schedulerResumeError(WrenValue* fiber, const char* error); -void schedulerReleaseMethods(); +void schedulerShutdown(); #endif diff --git a/test/io/stdin/aborts_on_eof.wren b/test/io/stdin/aborts_on_eof.wren new file mode 100644 index 00000000..630f0651 --- /dev/null +++ b/test/io/stdin/aborts_on_eof.wren @@ -0,0 +1,9 @@ +import "io" for Stdin + +Stdin.readLine() // stdin: one line + +var error = Fiber.new { + Stdin.readLine() +}.try() + +System.print(error) // expect: Stdin was closed. diff --git a/test/io/stdin/read_line.wren b/test/io/stdin/read_line.wren new file mode 100644 index 00000000..f0e3e869 --- /dev/null +++ b/test/io/stdin/read_line.wren @@ -0,0 +1,11 @@ +import "io" for Stdin + +System.write("> ") +System.print("1 " + Stdin.readLine()) +System.write("> ") +System.print("2 " + Stdin.readLine()) + +// stdin: first +// stdin: second +// expect: > 1 first +// expect: > 2 second diff --git a/util/test.py b/util/test.py index 9b922c6c..7105bde3 100755 --- a/util/test.py +++ b/util/test.py @@ -83,7 +83,7 @@ class Test: match = STDIN_PATTERN.search(line) if match: - input_lines.append(match.group(1) + '\n') + input_lines.append(match.group(1)) match = SKIP_PATTERN.search(line) if match: @@ -99,9 +99,9 @@ class Test: line_num += 1 - # If any input is fed to the test in stdin, concatetate it into one string. + # If any input is fed to the test in stdin, concatenate it into one string. if input_lines: - self.input_bytes = "".join(input_lines).encode("utf-8") + self.input_bytes = "\n".join(input_lines).encode("utf-8") # If we got here, it's a valid test. return True diff --git a/util/xcode/wren.xcodeproj/project.pbxproj b/util/xcode/wren.xcodeproj/project.pbxproj index bab8bafd..3eb854ae 100644 --- a/util/xcode/wren.xcodeproj/project.pbxproj +++ b/util/xcode/wren.xcodeproj/project.pbxproj @@ -109,6 +109,7 @@ 29D009AD1B7E39A8000CE58C /* value.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = value.h; path = ../../test/api/value.h; sourceTree = ""; }; 29DE39511AC3A50A00987D41 /* wren_meta.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = wren_meta.c; path = ../../src/vm/wren_meta.c; sourceTree = ""; }; 29DE39521AC3A50A00987D41 /* wren_meta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = wren_meta.h; path = ../../src/vm/wren_meta.h; sourceTree = ""; }; + 29F384111BD19706002F84E0 /* io.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = io.h; path = ../../src/module/io.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -134,6 +135,7 @@ 2901D7611B74F3E20083A2C8 /* module */ = { isa = PBXGroup; children = ( + 29F384111BD19706002F84E0 /* io.h */, 29729F2E1BA70A620099CA20 /* io.c */, 29729F301BA70A620099CA20 /* io.wren.inc */, 291647C31BA5EA45006142EE /* scheduler.h */,