feat: part of the story api exposed via c bindings
This commit is contained in:
parent
a065e5bf46
commit
3afbbb6ec2
6 changed files with 748 additions and 16 deletions
142
build.zig
142
build.zig
|
|
@ -1,8 +1,7 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const use_llvm = true;
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const mod = b.addModule("ink", .{
|
||||
|
|
@ -21,11 +20,71 @@ pub fn build(b: *std.Build) void {
|
|||
}),
|
||||
// Added because of a switch on a corrupt value on git hash
|
||||
// a8a6ce8d0f14c1dd5b3ea42e46f3226bcbe11a87
|
||||
.use_llvm = true,
|
||||
.use_llvm = use_llvm,
|
||||
.use_lld = use_llvm,
|
||||
});
|
||||
const lib = b.addLibrary(.{
|
||||
.linkage = .static,
|
||||
.name = "ink",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/c_api.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "ink", .module = mod },
|
||||
},
|
||||
}),
|
||||
.use_llvm = use_llvm,
|
||||
.use_lld = use_llvm,
|
||||
});
|
||||
|
||||
const pc: *std.Build.Step.InstallFile = pc: {
|
||||
const file = b.addWriteFile("libink.pc", b.fmt(
|
||||
\\prefix={s}
|
||||
\\includedir=${{prefix}}/include
|
||||
\\libdir=${{prefix}}/lib
|
||||
\\
|
||||
\\Name: libink
|
||||
\\URL: https://codeberg.org/haxolotl/ink
|
||||
\\Description: TODO
|
||||
\\Version: 0.1.0
|
||||
\\Cflags: -I${{includedir}}
|
||||
\\Libs: -L${{libdir}} -link
|
||||
, .{b.install_prefix}));
|
||||
break :pc b.addInstallFileWithDir(
|
||||
file.getDirectory().path(b, "libink.pc"),
|
||||
.prefix,
|
||||
"share/pkgconfig/libink.pc",
|
||||
);
|
||||
};
|
||||
|
||||
const c_header = b.addInstallFileWithDir(
|
||||
b.path("include/ink.h"),
|
||||
.header,
|
||||
"ink/ink.h",
|
||||
);
|
||||
|
||||
const examples = try buildExamples(b, target, optimize, lib);
|
||||
|
||||
b.getInstallStep().dependOn(&lib.step);
|
||||
b.getInstallStep().dependOn(&c_header.step);
|
||||
b.getInstallStep().dependOn(&pc.step);
|
||||
b.installArtifact(lib);
|
||||
b.installArtifact(exe);
|
||||
|
||||
for (examples) |example_exe| {
|
||||
b.getInstallStep().dependOn(&b.addInstallArtifact(
|
||||
example_exe,
|
||||
.{
|
||||
.dest_dir = .{
|
||||
.override = .{
|
||||
.custom = "bin/example",
|
||||
},
|
||||
},
|
||||
},
|
||||
).step);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
|
@ -42,16 +101,8 @@ pub fn build(b: *std.Build) void {
|
|||
});
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
.use_llvm = use_llvm,
|
||||
.use_lld = use_llvm,
|
||||
});
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
const run_cover = b.addSystemCommand(&.{
|
||||
"kcov",
|
||||
|
|
@ -63,3 +114,72 @@ pub fn build(b: *std.Build) void {
|
|||
const cover_step = b.step("cover", "Generate test coverage report");
|
||||
cover_step.dependOn(&run_cover.step);
|
||||
}
|
||||
|
||||
fn buildExamples(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
c_lib_: ?*std.Build.Step.Compile,
|
||||
) ![]const *std.Build.Step.Compile {
|
||||
const alloc = b.allocator;
|
||||
var steps: std.ArrayList(*std.Build.Step.Compile) = .empty;
|
||||
defer steps.deinit(alloc);
|
||||
|
||||
var dir = try std.fs.cwd().openDir(try b.build_root.join(
|
||||
b.allocator,
|
||||
&.{"examples"},
|
||||
), .{ .iterate = true });
|
||||
defer dir.close();
|
||||
|
||||
var it = dir.iterate();
|
||||
while (try it.next()) |entry| {
|
||||
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
|
||||
if (index == 0) continue;
|
||||
|
||||
const name = entry.name[0..index];
|
||||
const is_zig = std.mem.eql(u8, entry.name[index + 1 ..], "zig");
|
||||
const exe: *std.Build.Step.Compile = if (is_zig) exe: {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = name,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path(b.fmt(
|
||||
"examples/{s}",
|
||||
.{entry.name},
|
||||
)),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
exe.root_module.addImport("ink", b.modules.get("ink").?);
|
||||
break :exe exe;
|
||||
} else exe: {
|
||||
const c_lib = c_lib_ orelse return error.UnsupportedPlatform;
|
||||
const exe = b.addExecutable(.{
|
||||
.name = name,
|
||||
.root_module = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
exe.linkLibC();
|
||||
exe.addIncludePath(b.path("include"));
|
||||
exe.addCSourceFile(.{
|
||||
.file = b.path(b.fmt(
|
||||
"examples/{s}",
|
||||
.{entry.name},
|
||||
)),
|
||||
.flags = &[_][]const u8{
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-pedantic",
|
||||
"-std=c99",
|
||||
"-D_POSIX_C_SOURCE=199309L",
|
||||
},
|
||||
});
|
||||
exe.linkLibrary(c_lib);
|
||||
break :exe exe;
|
||||
};
|
||||
try steps.append(alloc, exe);
|
||||
}
|
||||
return steps.toOwnedSlice(alloc);
|
||||
}
|
||||
|
|
|
|||
330
examples/ink-c-driver.c
Normal file
330
examples/ink-c-driver.c
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <ink.h>
|
||||
|
||||
struct option {
|
||||
char *name;
|
||||
int id;
|
||||
bool has_args;
|
||||
};
|
||||
|
||||
struct ink_source {
|
||||
uint8_t *bytes;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
enum {
|
||||
OPTION_UNKNOWN = -2,
|
||||
OPTION_OPERAND = -3
|
||||
};
|
||||
|
||||
enum {
|
||||
OPT_COLORS = 1000,
|
||||
OPT_COMPILE_ONLY,
|
||||
OPT_USE_STDIN,
|
||||
OPT_DUMP_AST,
|
||||
OPT_DUMP_IR,
|
||||
OPT_DUMP_STORY,
|
||||
OPT_VM_TRACING,
|
||||
OPT_HELP,
|
||||
};
|
||||
|
||||
static const struct option cli_options[] = {
|
||||
{"--colors", OPT_COLORS, false},
|
||||
{"--compile-only", OPT_COMPILE_ONLY, false},
|
||||
//{"--dump-ast", OPT_DUMP_AST, false},
|
||||
//{"--dump-ir", OPT_DUMP_AST, false},
|
||||
//{"--dump-story", OPT_DUMP_STORY, false},
|
||||
//{"--trace", OPT_VM_TRACING, false},
|
||||
{"--stdin", OPT_USE_STDIN, false},
|
||||
{"--help", OPT_HELP, false},
|
||||
{"-h", OPT_HELP, false},
|
||||
{0},
|
||||
};
|
||||
|
||||
static const char *usage_msg =
|
||||
"Usage: %s [OPTION]... [FILE]\n"
|
||||
"Load and execute an Ink story.\n\n"
|
||||
" -h, --help Print this message\n"
|
||||
" --colors Enable color output\n"
|
||||
" --compile-only Compile the story without executing\n"
|
||||
" --stdin Read source file from standard input\n"
|
||||
"\n";
|
||||
|
||||
static const struct option *_g_option_opts;
|
||||
static char *_g_arg_ptr;
|
||||
static char **_g_option_argv;
|
||||
static char *option_operand;
|
||||
static char *option_unknown_opt;
|
||||
|
||||
static void print_usage(const char *name)
|
||||
{
|
||||
fprintf(stderr, usage_msg, name);
|
||||
}
|
||||
|
||||
static void option_setopts(const struct option *opts, char **argv)
|
||||
{
|
||||
_g_option_argv = argv;
|
||||
_g_option_opts = opts;
|
||||
}
|
||||
|
||||
static int option_nextopt(void)
|
||||
{
|
||||
if (*_g_option_argv != NULL) {
|
||||
++_g_option_argv;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char **optstr_p = _g_option_argv;
|
||||
if (*optstr_p == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const struct option *p = _g_option_opts;
|
||||
while (p->id != 0) {
|
||||
if (!strcmp(*optstr_p, p->name)) {
|
||||
if (p->has_args) {
|
||||
_g_arg_ptr = optstr_p[1];
|
||||
++_g_option_argv;
|
||||
}
|
||||
|
||||
return p->id;
|
||||
} else if (**optstr_p != '-') {
|
||||
option_operand = *optstr_p;
|
||||
return OPTION_OPERAND;
|
||||
}
|
||||
|
||||
++p;
|
||||
}
|
||||
|
||||
option_unknown_opt = *optstr_p;
|
||||
return OPTION_UNKNOWN;
|
||||
}
|
||||
|
||||
static char *option_nextarg(void)
|
||||
{
|
||||
char *arg = _g_arg_ptr;
|
||||
char *arg_p = arg;
|
||||
|
||||
while (*arg_p != '\0' && *arg_p != ',') {
|
||||
++arg_p;
|
||||
}
|
||||
if (*arg_p == '\0') {
|
||||
_g_arg_ptr = arg_p;
|
||||
return arg;
|
||||
}
|
||||
|
||||
_g_arg_ptr = arg_p + 1;
|
||||
*arg_p = '\0';
|
||||
return arg;
|
||||
}
|
||||
|
||||
#define INK_SOURCE_BUF_MAX 1024
|
||||
|
||||
static const char *INK_FILE_EXT = ".ink";
|
||||
static const size_t INK_FILE_EXT_LENGTH = 4;
|
||||
|
||||
static int ink_read_file(const char *file_path, uint8_t **bytes, size_t *length)
|
||||
{
|
||||
size_t sz = 0, nr = 0;
|
||||
uint8_t *b = NULL;
|
||||
FILE *const fp = fopen(file_path, "rb");
|
||||
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Could not read file '%s'\n", file_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fseek(fp, 0u, SEEK_END);
|
||||
sz = (size_t)ftell(fp);
|
||||
fseek(fp, 0u, SEEK_SET);
|
||||
|
||||
b = malloc(sz + 1);
|
||||
if (!b) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
nr = fread(b, 1u, sz, fp);
|
||||
if (nr < sz) {
|
||||
fprintf(stderr, "Could not read file '%s'.\n", file_path);
|
||||
fclose(fp);
|
||||
free(b);
|
||||
return -1;
|
||||
}
|
||||
|
||||
b[nr] = '\0';
|
||||
*bytes = b;
|
||||
*length = sz;
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ink_source_free(struct ink_source *s)
|
||||
{
|
||||
free(s->bytes);
|
||||
s->bytes = NULL;
|
||||
s->length = 0;
|
||||
}
|
||||
|
||||
static int ink_source_load_stdin(struct ink_source *s)
|
||||
{
|
||||
uint8_t *tmp;
|
||||
char b[INK_SOURCE_BUF_MAX];
|
||||
|
||||
s->bytes = NULL;
|
||||
s->length = 0;
|
||||
|
||||
while (fgets(b, INK_SOURCE_BUF_MAX, stdin)) {
|
||||
const size_t len = s->length;
|
||||
const size_t buflen = strlen(b);
|
||||
|
||||
tmp = realloc(s->bytes, len + buflen + 1);
|
||||
if (!tmp) {
|
||||
ink_source_free(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
s->bytes = tmp;
|
||||
memcpy(s->bytes + len, b, buflen);
|
||||
s->length += buflen;
|
||||
s->bytes[s->length] = '\0';
|
||||
}
|
||||
if (ferror(stdin)) {
|
||||
ink_source_free(s);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ink_source_load(const char *file_path, struct ink_source *s)
|
||||
{
|
||||
const char *ext;
|
||||
const size_t namelen = strlen(file_path);
|
||||
|
||||
s->bytes = NULL;
|
||||
s->length = 0;
|
||||
|
||||
if (namelen < INK_FILE_EXT_LENGTH) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ext = file_path + namelen - INK_FILE_EXT_LENGTH;
|
||||
if (!(strncmp(ext, INK_FILE_EXT, INK_FILE_EXT_LENGTH) == 0)) {
|
||||
return -1;
|
||||
}
|
||||
return ink_read_file(file_path, &s->bytes, &s->length);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
bool compile_only = false;
|
||||
bool use_stdin = false;
|
||||
int opt = 0;
|
||||
int rc = -1;
|
||||
int flags = 0;
|
||||
const char *filename = NULL;
|
||||
struct ink_source source;
|
||||
struct ink_story *story = NULL;
|
||||
|
||||
option_setopts(cli_options, argv);
|
||||
|
||||
while ((opt = option_nextopt())) {
|
||||
switch (opt) {
|
||||
case OPT_COLORS:
|
||||
flags |= INK_F_USE_COLOR;
|
||||
break;
|
||||
case OPT_DUMP_AST:
|
||||
flags |= INK_F_DUMP_AST;
|
||||
break;
|
||||
case OPT_DUMP_IR:
|
||||
flags |= INK_F_DUMP_IR;
|
||||
break;
|
||||
case OPT_DUMP_STORY:
|
||||
flags |= INK_F_DUMP_CODE;
|
||||
break;
|
||||
case OPT_COMPILE_ONLY:
|
||||
compile_only = true;
|
||||
break;
|
||||
case OPT_USE_STDIN:
|
||||
use_stdin = true;
|
||||
break;
|
||||
case OPTION_UNKNOWN:
|
||||
fprintf(stderr, "Unrecognised option %s.\n\n", option_unknown_opt);
|
||||
print_usage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
case OPT_HELP:
|
||||
print_usage(argv[0]);
|
||||
return EXIT_SUCCESS;
|
||||
case OPTION_OPERAND:
|
||||
filename = option_operand;
|
||||
break;
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
if (filename == NULL || *filename == '\0') {
|
||||
if (use_stdin) {
|
||||
rc = ink_source_load_stdin(&source);
|
||||
filename = "<STDIN>";
|
||||
} else {
|
||||
print_usage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
} else {
|
||||
rc = ink_source_load(filename, &source);
|
||||
}
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "Error while loading source file.\n");
|
||||
return rc;
|
||||
}
|
||||
|
||||
story = ink_open();
|
||||
if (!story) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
const struct ink_story_options opts = {
|
||||
.source_bytes = source.bytes,
|
||||
.source_length = source.length,
|
||||
.filename = (uint8_t *)filename,
|
||||
.filename_length = strlen(filename),
|
||||
.flags = flags,
|
||||
};
|
||||
|
||||
rc = ink_load_story_options(story, &opts);
|
||||
if (rc < 0) {
|
||||
goto out;
|
||||
}
|
||||
if (!compile_only) {
|
||||
while (ink_story_can_continue(story)) {
|
||||
const char *line = NULL;
|
||||
size_t linelen = 0;
|
||||
size_t choice_index = 0;
|
||||
|
||||
while (ink_story_continue(story, &line, &linelen)) {
|
||||
if (line) {
|
||||
printf("%.*s\n", (int)linelen, line);
|
||||
}
|
||||
}
|
||||
while (ink_story_choice_next(story, &line, &linelen)) {
|
||||
printf("%zu: %.*s\n", ++choice_index, (int)linelen, line);
|
||||
}
|
||||
if (choice_index > 0) {
|
||||
printf("> ");
|
||||
scanf("%zu", &choice_index);
|
||||
ink_story_choose(story, choice_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
out:
|
||||
ink_close(story);
|
||||
ink_source_free(&source);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
122
include/ink.h
Normal file
122
include/ink.h
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#ifndef INK_API_H
|
||||
#define INK_API_H
|
||||
|
||||
/**
|
||||
* @defgroup ink_api API
|
||||
*
|
||||
* This module documents the exported functions that form the public API
|
||||
* of libink.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
#ifdef BUILDING_INKLIB
|
||||
#define INK_API __declspec(dllexport)
|
||||
#else
|
||||
#define INK_API __declspec(dllimport)
|
||||
#endif
|
||||
#else
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define INK_API __attribute__((visibility("default")))
|
||||
#else
|
||||
#define INK_API
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @struct ink_story
|
||||
*
|
||||
* @brief Opaque type representing a story context.
|
||||
*
|
||||
* This forward declaration hides the internal structure of an `ink_story`.
|
||||
* Clients of the API should manipulate `ink_story` objects only through the
|
||||
* provided functions.
|
||||
*/
|
||||
typedef struct ink_story ink_story;
|
||||
|
||||
struct ink_story_options {
|
||||
const uint8_t *filename;
|
||||
size_t filename_length;
|
||||
const uint8_t *source_bytes;
|
||||
size_t source_length;
|
||||
int flags;
|
||||
};
|
||||
|
||||
enum ink_flags {
|
||||
INK_F_RESERVED_1 = (1 << 0),
|
||||
INK_F_RESERVED_2 = (1 << 1),
|
||||
INK_F_RESERVED_3 = (1 << 2),
|
||||
INK_F_RESERVED_4 = (1 << 3),
|
||||
INK_F_USE_COLOR = (1 << 4),
|
||||
INK_F_DUMP_AST = (1 << 5),
|
||||
INK_F_DUMP_IR = (1 << 6),
|
||||
INK_F_DUMP_CODE = (1 << 7),
|
||||
INK_F_RESERVED_9 = (1 << 8),
|
||||
INK_F_RESERVED_10 = (1 << 9),
|
||||
INK_F_RESERVED_11 = (1 << 10),
|
||||
INK_F_RESERVED_12 = (1 << 11),
|
||||
INK_F_RESERVED_13 = (1 << 12),
|
||||
INK_F_RESERVED_14 = (1 << 13),
|
||||
INK_F_RESERVED_15 = (1 << 14),
|
||||
INK_F_RESERVED_16 = (1 << 15),
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Open a story context.
|
||||
*
|
||||
* @returns a new story context
|
||||
*/
|
||||
INK_API struct ink_story *ink_open(void);
|
||||
|
||||
/**
|
||||
* Close a story context.
|
||||
*/
|
||||
INK_API void ink_close(struct ink_story *story);
|
||||
|
||||
/**
|
||||
* Load an Ink story with extended options.
|
||||
*
|
||||
* @returns a non-zero value on error.
|
||||
*/
|
||||
INK_API int ink_load_story_options(struct ink_story *story,
|
||||
const struct ink_load_options *options);
|
||||
|
||||
/**
|
||||
* Determine if the story can continue.
|
||||
*
|
||||
* @returns a boolean value, indicating if the story can continue.
|
||||
*/
|
||||
INK_API bool ink_story_can_continue(struct ink_story *story);
|
||||
|
||||
/**
|
||||
* Advance the story and output content, if available.
|
||||
*
|
||||
* @returns a non-zero value on error.
|
||||
*/
|
||||
INK_API int ink_story_continue(struct ink_story *story, uint8_t **line,
|
||||
size_t *linelen);
|
||||
|
||||
INK_API int ink_story_choice_next(struct ink_story *story, uint8_t **line,
|
||||
size_t *linelen);
|
||||
|
||||
/**
|
||||
* Select a choice by its index.
|
||||
*
|
||||
* @returns a non-zero value on error.
|
||||
*/
|
||||
INK_API int ink_story_choose(struct ink_story *story, size_t index);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
|
@ -38,6 +38,7 @@ constants_pool: []const Value = &.{},
|
|||
/// Interned string bytes.
|
||||
string_bytes: []const u8 = &.{},
|
||||
dump_writer: ?*std.Io.Writer = null,
|
||||
internal_counter: usize = 0,
|
||||
|
||||
pub const default_knot_name: [:0]const u8 = "$__main__$";
|
||||
|
||||
|
|
@ -372,23 +373,23 @@ pub fn deinit(story: *Story) void {
|
|||
story.* = undefined;
|
||||
}
|
||||
|
||||
fn currentFrame(vm: *Story) *CallFrame {
|
||||
pub fn currentFrame(vm: *Story) *CallFrame {
|
||||
assert(vm.call_stack_top > 0);
|
||||
return &vm.call_stack[vm.call_stack_top - 1];
|
||||
}
|
||||
|
||||
fn peekStack(vm: *Story, offset: usize) ?Value {
|
||||
pub fn peekStack(vm: *Story, offset: usize) ?Value {
|
||||
if (vm.stack_top <= offset) return null;
|
||||
return vm.stack[vm.stack_top - offset - 1];
|
||||
}
|
||||
|
||||
fn pushStack(vm: *Story, value: Value) !void {
|
||||
pub fn pushStack(vm: *Story, value: Value) !void {
|
||||
if (vm.stack_top >= vm.stack.len) return error.StackOverflow;
|
||||
vm.stack[vm.stack_top] = value;
|
||||
vm.stack_top += 1;
|
||||
}
|
||||
|
||||
fn popStack(vm: *Story) ?Value {
|
||||
pub fn popStack(vm: *Story) ?Value {
|
||||
if (vm.stack_top == 0) return null;
|
||||
|
||||
const stack_top = vm.stack_top;
|
||||
|
|
@ -489,7 +490,7 @@ fn divertValue(vm: *Story, value: Value, args_count: u8) !void {
|
|||
}
|
||||
|
||||
// Diverts are essentially tail calls.
|
||||
fn divert(vm: *Story, knot: *Object.Knot, args_count: u8) !void {
|
||||
pub fn divert(vm: *Story, knot: *Object.Knot, args_count: u8) !void {
|
||||
assert(knot.code.args_count == args_count);
|
||||
if (vm.call_stack_top == 0)
|
||||
return vm.call(knot, args_count);
|
||||
|
|
|
|||
158
src/c_api.zig
Normal file
158
src/c_api.zig
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
const std = @import("std");
|
||||
const ink = @import("ink");
|
||||
const Story = ink.Story;
|
||||
const Module = ink.Module;
|
||||
|
||||
var global_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
var stdout_buffer: [4096]u8 align(std.heap.page_size_min) = undefined;
|
||||
|
||||
pub export fn ink_open() callconv(.c) ?*Story {
|
||||
const gpa = global_allocator.allocator();
|
||||
const story = gpa.create(Story) catch |err| switch (err) {
|
||||
error.OutOfMemory => return null,
|
||||
};
|
||||
story.* = .{
|
||||
.gpa = gpa,
|
||||
.arena = .init(gpa),
|
||||
.is_exited = false,
|
||||
.can_advance = false,
|
||||
.stack_top = 0,
|
||||
.call_stack_top = 0,
|
||||
.output_marker = 0,
|
||||
.choice_selected = null,
|
||||
.output_buffer = .empty,
|
||||
.output_scratch = .empty,
|
||||
.current_choices = .empty,
|
||||
.variable_observers = .empty,
|
||||
.globals = .empty,
|
||||
.stack = &.{},
|
||||
.call_stack = &.{},
|
||||
.code_chunks = .empty,
|
||||
.gc_objects = .{},
|
||||
.constants_pool = &.{},
|
||||
.string_bytes = &.{},
|
||||
.dump_writer = null,
|
||||
};
|
||||
return story;
|
||||
}
|
||||
|
||||
pub export fn ink_close(story: *Story) callconv(.c) void {
|
||||
defer _ = global_allocator.deinit();
|
||||
const gpa = story.gpa;
|
||||
story.deinit();
|
||||
gpa.destroy(story);
|
||||
}
|
||||
|
||||
pub const InkLoadOpts = extern struct {
|
||||
filename: [*]const u8,
|
||||
filename_length: usize,
|
||||
source_bytes: [*]const u8,
|
||||
source_length: usize,
|
||||
flags: i32,
|
||||
};
|
||||
|
||||
fn loadStory(story: *Story, options: *const InkLoadOpts) !void {
|
||||
const gpa = story.gpa;
|
||||
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
|
||||
defer arena_allocator.deinit();
|
||||
|
||||
const arena = arena_allocator.allocator();
|
||||
const stderr = std.fs.File.stderr();
|
||||
var stderr_writer = stderr.writer(&stdout_buffer);
|
||||
|
||||
var comp = try Module.compile(gpa, arena, .{
|
||||
.source_bytes = options.source_bytes[0..options.source_length :0],
|
||||
.filename = options.filename[0..options.filename_length],
|
||||
.dump_writer = null,
|
||||
.dump_use_color = false,
|
||||
.dump_ast = false,
|
||||
.dump_ir = false,
|
||||
});
|
||||
defer comp.deinit();
|
||||
|
||||
if (comp.errors.items.len > 0) {
|
||||
for (comp.errors.items) |err| {
|
||||
try comp.renderError(&stderr_writer.interface, err);
|
||||
}
|
||||
return error.LoadFailed;
|
||||
}
|
||||
|
||||
// TODO: Make this configureable.
|
||||
const stack_size = 128;
|
||||
const eval_stack_ptr = try gpa.alloc(Story.Value, stack_size);
|
||||
errdefer gpa.free(eval_stack_ptr);
|
||||
|
||||
const call_stack_ptr = try gpa.alloc(Story.CallFrame, stack_size);
|
||||
errdefer gpa.free(call_stack_ptr);
|
||||
|
||||
story.can_advance = false;
|
||||
story.stack = eval_stack_ptr;
|
||||
story.call_stack = call_stack_ptr;
|
||||
errdefer story.deinit();
|
||||
|
||||
try comp.setupStoryRuntime(gpa, story);
|
||||
if (story.getKnot(Story.default_knot_name)) |knot| {
|
||||
try story.pushStack(.{ .object = &knot.base });
|
||||
try story.divert(knot, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub export fn ink_load_story_options(
|
||||
story: *Story,
|
||||
options: *const InkLoadOpts,
|
||||
) callconv(.c) c_int {
|
||||
loadStory(story, options) catch |err| {
|
||||
std.debug.print("{any}\n", .{@errorName(err)});
|
||||
return -1;
|
||||
};
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub export fn ink_story_can_continue(story: *Story) callconv(.c) bool {
|
||||
return !story.is_exited and story.can_advance;
|
||||
}
|
||||
|
||||
pub export fn ink_story_continue(
|
||||
story: *Story,
|
||||
line: *?[*]const u8,
|
||||
linelen: *usize,
|
||||
) callconv(.c) c_int {
|
||||
const result = story.advance() catch return -1;
|
||||
if (result) |slice| {
|
||||
line.* = slice.ptr;
|
||||
linelen.* = slice.len;
|
||||
return 1;
|
||||
} else {
|
||||
line.* = null;
|
||||
linelen.* = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: The internal counter may be a bad idea...
|
||||
pub export fn ink_story_choice_next(
|
||||
story: *Story,
|
||||
line: *?[*]const u8,
|
||||
linelen: *usize,
|
||||
) callconv(.c) c_int {
|
||||
if (story.internal_counter < story.current_choices.items.len) {
|
||||
const choice = &story.current_choices.items[story.internal_counter];
|
||||
line.* = choice.content.ptr;
|
||||
linelen.* = choice.content.len;
|
||||
story.internal_counter += 1;
|
||||
return 1;
|
||||
} else {
|
||||
line.* = null;
|
||||
linelen.* = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: The internal counter may be a bad idea...
|
||||
pub export fn ink_story_choose(story: *Story, index: usize) callconv(.c) c_int {
|
||||
story.selectChoiceIndex(index) catch {
|
||||
return -1;
|
||||
};
|
||||
story.internal_counter = 0;
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||
const tokenizer = @import("tokenizer.zig");
|
||||
pub const Story = @import("Story.zig");
|
||||
pub const Ast = @import("Ast.zig");
|
||||
pub const Module = @import("compile.zig").Module;
|
||||
|
||||
pub const max_src_size = std.math.maxInt(u32);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue