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");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) !void {
|
||||||
const use_llvm = true;
|
const use_llvm = true;
|
||||||
|
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
const mod = b.addModule("ink", .{
|
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
|
// Added because of a switch on a corrupt value on git hash
|
||||||
// a8a6ce8d0f14c1dd5b3ea42e46f3226bcbe11a87
|
// 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);
|
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_step = b.step("run", "Run the app");
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
run_step.dependOn(&run_cmd.step);
|
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 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");
|
const test_step = b.step("test", "Run tests");
|
||||||
test_step.dependOn(&run_mod_tests.step);
|
test_step.dependOn(&run_mod_tests.step);
|
||||||
test_step.dependOn(&run_exe_tests.step);
|
|
||||||
|
|
||||||
const run_cover = b.addSystemCommand(&.{
|
const run_cover = b.addSystemCommand(&.{
|
||||||
"kcov",
|
"kcov",
|
||||||
|
|
@ -63,3 +114,72 @@ pub fn build(b: *std.Build) void {
|
||||||
const cover_step = b.step("cover", "Generate test coverage report");
|
const cover_step = b.step("cover", "Generate test coverage report");
|
||||||
cover_step.dependOn(&run_cover.step);
|
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.
|
/// Interned string bytes.
|
||||||
string_bytes: []const u8 = &.{},
|
string_bytes: []const u8 = &.{},
|
||||||
dump_writer: ?*std.Io.Writer = null,
|
dump_writer: ?*std.Io.Writer = null,
|
||||||
|
internal_counter: usize = 0,
|
||||||
|
|
||||||
pub const default_knot_name: [:0]const u8 = "$__main__$";
|
pub const default_knot_name: [:0]const u8 = "$__main__$";
|
||||||
|
|
||||||
|
|
@ -372,23 +373,23 @@ pub fn deinit(story: *Story) void {
|
||||||
story.* = undefined;
|
story.* = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn currentFrame(vm: *Story) *CallFrame {
|
pub fn currentFrame(vm: *Story) *CallFrame {
|
||||||
assert(vm.call_stack_top > 0);
|
assert(vm.call_stack_top > 0);
|
||||||
return &vm.call_stack[vm.call_stack_top - 1];
|
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;
|
if (vm.stack_top <= offset) return null;
|
||||||
return vm.stack[vm.stack_top - offset - 1];
|
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;
|
if (vm.stack_top >= vm.stack.len) return error.StackOverflow;
|
||||||
vm.stack[vm.stack_top] = value;
|
vm.stack[vm.stack_top] = value;
|
||||||
vm.stack_top += 1;
|
vm.stack_top += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn popStack(vm: *Story) ?Value {
|
pub fn popStack(vm: *Story) ?Value {
|
||||||
if (vm.stack_top == 0) return null;
|
if (vm.stack_top == 0) return null;
|
||||||
|
|
||||||
const stack_top = vm.stack_top;
|
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.
|
// 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);
|
assert(knot.code.args_count == args_count);
|
||||||
if (vm.call_stack_top == 0)
|
if (vm.call_stack_top == 0)
|
||||||
return vm.call(knot, args_count);
|
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");
|
const tokenizer = @import("tokenizer.zig");
|
||||||
pub const Story = @import("Story.zig");
|
pub const Story = @import("Story.zig");
|
||||||
pub const Ast = @import("Ast.zig");
|
pub const Ast = @import("Ast.zig");
|
||||||
|
pub const Module = @import("compile.zig").Module;
|
||||||
|
|
||||||
pub const max_src_size = std.math.maxInt(u32);
|
pub const max_src_size = std.math.maxInt(u32);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue