feat: part of the story api exposed via c bindings

This commit is contained in:
Brett Broadhurst 2026-04-13 12:10:01 -06:00
parent a065e5bf46
commit 3afbbb6ec2
Failed to generate hash of commit
6 changed files with 748 additions and 16 deletions

158
src/c_api.zig Normal file
View 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;
}