libghostty-vt C shared library boilerplate, custom allocators API (#8895)
This adds the boilerplate necessary for `libghostty-vt` the C library. > [!IMPORTANT] > > This _does not expose almost any APIs_. The point of this PR is to setup our boilerplate, build system, docs system, etc. for the libghostty-vt C lib. - Adds a `zig build lib-vt` target to _only_ build `libghostty-vt` - Adds the beginning of `include/ghostty-vt.h` for the C API - Adds a full custom allocator interface to the C API mimicking Zig custom allocators - Adds an example in `example/c-vt` that builds a pure C program and links to our shared library and calls functions - Adds the `osc_parser_new/free` C APIs just as a proof of concept that things work - Adds a basic Doxygen config so we have _something_ (I'm not at all committed to Doxygen, but want us to doc from the beginning) - Updates CI to test building the shared library for macOS, Windows, and Linux (yes, it builds for Windows!) **Note:** To use the `dep.artifact` function provided by Zig, we must install the artifact. But this means that every `zig build` now includes `libghostty-vt`. That... could be completely fine, but it's something to consider for packagers. ## Bikeshed We're at a pivotal point where we must define the general _style_ of our C API. This includes the very bike shed things such as capitalization styling, but also general API form. ABI compatibility will eventually be important. I'm very much open and would love to receive feedback form more experience C programmers on what they feel would constitute a good API. I've consumed _many_ C APIs but I haven't provided many directly. cc @gpanderspull/8901/head
commit
390f72accc
|
|
@ -15,6 +15,7 @@ jobs:
|
|||
- build-examples
|
||||
- build-flatpak
|
||||
- build-freebsd
|
||||
- build-libghostty-vt
|
||||
- build-linux
|
||||
- build-linux-libghostty
|
||||
- build-nix
|
||||
|
|
@ -94,7 +95,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dir: [zig-vt]
|
||||
dir: [c-vt, zig-vt]
|
||||
name: Example ${{ matrix.dir }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
@ -194,6 +195,48 @@ jobs:
|
|||
zig build \
|
||||
-Dsnap
|
||||
|
||||
build-libghostty-vt:
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
[
|
||||
aarch64-macos,
|
||||
x86_64-macos,
|
||||
aarch64-linux,
|
||||
x86_64-linux,
|
||||
x86_64-windows,
|
||||
]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@a809471b5c7c913aa67bec8f459a11a0decc3fce # v31.6.2
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix develop -c zig build lib-vt \
|
||||
-Dtarget=${{ matrix.target }} \
|
||||
-Dsimd=false
|
||||
|
||||
build-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# Doxyfile 1.13.2
|
||||
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = "libghostty"
|
||||
INPUT = include/ghostty/vt.h
|
||||
INPUT_ENCODING = UTF-8
|
||||
RECURSIVE = NO
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# HTML Output
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Man Output
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = zig-out/share/man
|
||||
MAN_EXTENSION = .3
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Other Output
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
GENERATE_LATEX = NO
|
||||
11
build.zig
11
build.zig
|
|
@ -31,6 +31,7 @@ pub fn build(b: *std.Build) !void {
|
|||
|
||||
// All our steps which we'll hook up later. The steps are shown
|
||||
// up here just so that they are more self-documenting.
|
||||
const libvt_step = b.step("lib-vt", "Build libghostty-vt");
|
||||
const run_step = b.step("run", "Run the app");
|
||||
const run_valgrind_step = b.step(
|
||||
"run-valgrind",
|
||||
|
|
@ -86,7 +87,7 @@ pub fn build(b: *std.Build) !void {
|
|||
check_step.dependOn(dist.install_step);
|
||||
}
|
||||
|
||||
// libghostty
|
||||
// libghostty (internal, big)
|
||||
const libghostty_shared = try buildpkg.GhosttyLib.initShared(
|
||||
b,
|
||||
&deps,
|
||||
|
|
@ -96,6 +97,14 @@ pub fn build(b: *std.Build) !void {
|
|||
&deps,
|
||||
);
|
||||
|
||||
// libghostty-vt
|
||||
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
libghostty_vt_shared.install(libvt_step);
|
||||
libghostty_vt_shared.install(b.getInstallStep());
|
||||
|
||||
// Helpgen
|
||||
if (config.emit_helpgen) deps.help_strings.install();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Example: `ghostty-vt` C Program
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` C library
|
||||
with a C program.
|
||||
|
||||
This uses a `build.zig` and `Zig` to build the C program so that we
|
||||
can reuse a lot of our build logic and depend directly on our source
|
||||
tree, but Ghostty emits a standard C library that can be used with any
|
||||
C tooling.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the program:
|
||||
|
||||
```shell-session
|
||||
zig build run
|
||||
```
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
const exe_mod = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_mod.addCSourceFiles(.{
|
||||
.root = b.path("src"),
|
||||
.files = &.{"main.c"},
|
||||
});
|
||||
|
||||
// You'll want to use a lazy dependency here so that ghostty is only
|
||||
// downloaded if you actually need it.
|
||||
if (b.lazyDependency("ghostty", .{
|
||||
// Setting simd to false will force a pure static build that
|
||||
// doesn't even require libc, but it has a significant performance
|
||||
// penalty. If your embedding app requires libc anyway, you should
|
||||
// always keep simd enabled.
|
||||
// .simd = false,
|
||||
})) |dep| {
|
||||
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
|
||||
}
|
||||
|
||||
// Exe
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "c_vt",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.{
|
||||
.name = .c_vt,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x413a8529b1255f9a,
|
||||
.minimum_zig_version = "0.14.1",
|
||||
.dependencies = .{
|
||||
// Ghostty dependency. In reality, you'd probably use a URL-based
|
||||
// dependency like the one showed (and commented out) below this one.
|
||||
// We use a path dependency here for simplicity and to ensure our
|
||||
// examples always test against the source they're bundled with.
|
||||
.ghostty = .{ .path = "../../" },
|
||||
|
||||
// Example of what a URL-based dependency looks like:
|
||||
// .ghostty = .{
|
||||
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
|
||||
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
|
||||
// },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
#include <stddef.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
int main() {
|
||||
GhosttyOscParser parser;
|
||||
if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) {
|
||||
return 1;
|
||||
}
|
||||
ghostty_osc_free(parser);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* @file vt.h
|
||||
*
|
||||
* libghostty-vt - Virtual terminal sequence parsing library
|
||||
*
|
||||
* This library provides functionality for parsing and handling terminal
|
||||
* escape sequences as well as maintaining terminal state such as styles,
|
||||
* cursor position, screen, scrollback, and more.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_H
|
||||
#define GHOSTTY_VT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
/**
|
||||
* Opaque handle to an OSC parser instance.
|
||||
*
|
||||
* This handle represents an OSC (Operating System Command) parser that can
|
||||
* be used to parse the contents of OSC sequences. This isn't a full VT
|
||||
* parser; it is only the OSC parser component. This is useful if you have
|
||||
* a parser already and want to only extract and handle OSC sequences.
|
||||
*/
|
||||
typedef struct GhosttyOscParser *GhosttyOscParser;
|
||||
|
||||
/**
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
typedef enum {
|
||||
/** Operation completed successfully */
|
||||
GHOSTTY_SUCCESS = 0,
|
||||
/** Operation failed due to failed allocation */
|
||||
GHOSTTY_OUT_OF_MEMORY = -1,
|
||||
} GhosttyResult;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Allocator Interface
|
||||
|
||||
/**
|
||||
* Function table for custom memory allocator operations.
|
||||
*
|
||||
* This vtable defines the interface for a custom memory allocator. All
|
||||
* function pointers must be valid and non-NULL.
|
||||
*
|
||||
* If you're not going to use a custom allocator, you can ignore all of
|
||||
* this. All functions that take an allocator pointer allow NULL to use a
|
||||
* default allocator.
|
||||
*
|
||||
* The interface is based on the Zig allocator interface. I'll say up front
|
||||
* that it is easy to look at this interface and think "wow, this is really
|
||||
* overcomplicated". The reason for this complexity is well thought out by
|
||||
* the Zig folks, and it enables a diverse set of allocation strategies
|
||||
* as shown by the Zig ecosystem. As a consolation, please note that many
|
||||
* of the arguments are only needed for advanced use cases and can be
|
||||
* safely ignored in simple implementations. For example, if you look at
|
||||
* the Zig implementation of the libc allocator in `lib/std/heap.zig`
|
||||
* (search for CAllocator), you'll see it is very simple.
|
||||
*
|
||||
* NOTE(mitchellh): In the future, we can have default implementations of
|
||||
* resize/remap and allow those to be null.
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Return a pointer to `len` bytes with specified `alignment`, or return
|
||||
* `NULL` indicating the allocation failed.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param len Number of bytes to allocate
|
||||
* @param alignment Required alignment for the allocation. Guaranteed to
|
||||
* be a power of two between 1 and 16 inclusive.
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to allocated memory, or NULL if allocation failed
|
||||
*/
|
||||
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory in place.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to resize
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return true if resize was successful in-place, false if relocation would be required
|
||||
*/
|
||||
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory, allowing relocation.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* A non-`NULL` return value indicates the resize was successful. The
|
||||
* allocation may have same address, or may have been relocated. In either
|
||||
* case, the allocation now has size of `new_len`. A `NULL` return value
|
||||
* indicates that the resize would be equivalent to allocating new memory,
|
||||
* copying the bytes from the old memory, and then freeing the old memory.
|
||||
* In such case, it is more efficient for the caller to perform the copy.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to remap
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
|
||||
*/
|
||||
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Free and invalidate a region of memory.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to free
|
||||
* @param memory_len Size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
*/
|
||||
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
|
||||
} GhosttyAllocatorVtable;
|
||||
|
||||
/**
|
||||
* Custom memory allocator.
|
||||
*
|
||||
* For functions that take an allocator pointer, a NULL pointer indicates
|
||||
* that the default allocator should be used. The default allocator will
|
||||
* be libc malloc/free if we're linking to libc. If libc isn't linked,
|
||||
* a custom allocator is used (currently Zig's SMP allocator).
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* GhosttyAllocator allocator = {
|
||||
* .vtable = &my_allocator_vtable,
|
||||
* .ctx = my_allocator_state
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Opaque context pointer passed to all vtable functions.
|
||||
* This allows the allocator implementation to maintain state
|
||||
* or reference external resources needed for memory management.
|
||||
*/
|
||||
void *ctx;
|
||||
|
||||
/**
|
||||
* Pointer to the allocator's vtable containing function pointers
|
||||
* for memory operations (alloc, resize, remap, free).
|
||||
*/
|
||||
const GhosttyAllocatorVtable *vtable;
|
||||
} GhosttyAllocator;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Create a new OSC parser instance.
|
||||
*
|
||||
* Creates a new OSC (Operating System Command) parser using the provided
|
||||
* allocator. The parser must be freed using ghostty_vt_osc_free() when
|
||||
* no longer needed.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
|
||||
* @param parser Pointer to store the created parser handle
|
||||
* @return GHOSTTY_VT_SUCCESS on success, or an error code on failure
|
||||
*/
|
||||
GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
|
||||
|
||||
/**
|
||||
* Free an OSC parser instance.
|
||||
*
|
||||
* Releases all resources associated with the OSC parser. After this call,
|
||||
* the parser handle becomes invalid and must not be used.
|
||||
*
|
||||
* @param parser The parser handle to free (may be NULL)
|
||||
*/
|
||||
void ghostty_osc_free(GhosttyOscParser parser);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GHOSTTY_VT_H */
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
lib,
|
||||
stdenv,
|
||||
bashInteractive,
|
||||
doxygen,
|
||||
nushell,
|
||||
appstream,
|
||||
flatpak-builder,
|
||||
|
|
@ -89,6 +90,7 @@ in
|
|||
packages =
|
||||
[
|
||||
# For builds
|
||||
doxygen
|
||||
jq
|
||||
llvmPackages_latest.llvm
|
||||
minisign
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
const GhosttyLibVt = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const RunStep = std.Build.Step.Run;
|
||||
const Config = @import("Config.zig");
|
||||
const GhosttyZig = @import("GhosttyZig.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
const LipoStep = @import("LipoStep.zig");
|
||||
|
||||
/// The step that generates the file.
|
||||
step: *std.Build.Step,
|
||||
|
||||
/// The artifact result
|
||||
artifact: *std.Build.Step.InstallArtifact,
|
||||
|
||||
/// The final library file
|
||||
output: std.Build.LazyPath,
|
||||
dsym: ?std.Build.LazyPath,
|
||||
pkg_config: std.Build.LazyPath,
|
||||
|
||||
pub fn initShared(
|
||||
b: *std.Build,
|
||||
zig: *const GhosttyZig,
|
||||
) !GhosttyLibVt {
|
||||
const target = zig.vt.resolved_target.?;
|
||||
const lib = b.addSharedLibrary(.{
|
||||
.name = "ghostty-vt",
|
||||
.root_module = zig.vt,
|
||||
});
|
||||
lib.installHeader(
|
||||
b.path("include/ghostty/vt.h"),
|
||||
"ghostty/vt.h",
|
||||
);
|
||||
|
||||
// Get our debug symbols
|
||||
const dsymutil: ?std.Build.LazyPath = dsymutil: {
|
||||
if (!target.result.os.tag.isDarwin()) {
|
||||
break :dsymutil null;
|
||||
}
|
||||
|
||||
const dsymutil = RunStep.create(b, "dsymutil");
|
||||
dsymutil.addArgs(&.{"dsymutil"});
|
||||
dsymutil.addFileArg(lib.getEmittedBin());
|
||||
dsymutil.addArgs(&.{"-o"});
|
||||
const output = dsymutil.addOutputFileArg("libghostty-vt.dSYM");
|
||||
break :dsymutil output;
|
||||
};
|
||||
|
||||
// pkg-config
|
||||
const pc: std.Build.LazyPath = pc: {
|
||||
const wf = b.addWriteFiles();
|
||||
break :pc wf.add("libghostty-vt.pc", b.fmt(
|
||||
\\prefix={s}
|
||||
\\includedir=${{prefix}}/include
|
||||
\\libdir=${{prefix}}/lib
|
||||
\\
|
||||
\\Name: libghostty-vt
|
||||
\\URL: https://github.com/ghostty-org/ghostty
|
||||
\\Description: Ghostty VT library
|
||||
\\Version: 0.1.0
|
||||
\\Cflags: -I${{includedir}}
|
||||
\\Libs: -L${{libdir}} -lghostty-vt
|
||||
, .{b.install_prefix}));
|
||||
};
|
||||
|
||||
return .{
|
||||
.step = &lib.step,
|
||||
.artifact = b.addInstallArtifact(lib, .{}),
|
||||
.output = lib.getEmittedBin(),
|
||||
.dsym = dsymutil,
|
||||
.pkg_config = pc,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn install(
|
||||
self: *const GhosttyLibVt,
|
||||
step: *std.Build.Step,
|
||||
) void {
|
||||
const b = step.owner;
|
||||
step.dependOn(&self.artifact.step);
|
||||
step.dependOn(&b.addInstallFileWithDir(
|
||||
self.pkg_config,
|
||||
.prefix,
|
||||
"share/pkgconfig/libghostty-vt.pc",
|
||||
).step);
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
const SharedDeps = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Config = @import("Config.zig");
|
||||
const HelpStrings = @import("HelpStrings.zig");
|
||||
const MetallibStep = @import("MetallibStep.zig");
|
||||
|
|
@ -104,6 +106,19 @@ pub fn add(
|
|||
var static_libs = LazyPathList.init(b.allocator);
|
||||
errdefer static_libs.deinit();
|
||||
|
||||
// WARNING: This is a hack!
|
||||
// If we're cross-compiling to Darwin then we don't add any deps.
|
||||
// We don't support cross-compiling to Darwin but due to the way
|
||||
// lazy dependencies work with Zig, we call this function. So we just
|
||||
// bail. The build will fail but the build would've failed anyways.
|
||||
// And this lets other non-platform-specific targets like `lib-vt`
|
||||
// cross-compile properly.
|
||||
if (!builtin.target.os.tag.isDarwin() and
|
||||
self.config.target.result.os.tag.isDarwin())
|
||||
{
|
||||
return static_libs;
|
||||
}
|
||||
|
||||
// Every exe gets build options populated
|
||||
step.root_module.addOptions("build_options", self.options);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub const GhosttyDocs = @import("GhosttyDocs.zig");
|
|||
pub const GhosttyExe = @import("GhosttyExe.zig");
|
||||
pub const GhosttyFrameData = @import("GhosttyFrameData.zig");
|
||||
pub const GhosttyLib = @import("GhosttyLib.zig");
|
||||
pub const GhosttyLibVt = @import("GhosttyLibVt.zig");
|
||||
pub const GhosttyResources = @import("GhosttyResources.zig");
|
||||
pub const GhosttyI18n = @import("GhosttyI18n.zig");
|
||||
pub const GhosttyXcodebuild = @import("GhosttyXcodebuild.zig");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const testing = std.testing;
|
||||
|
||||
/// Useful alias since they're required to create Zig allocators
|
||||
pub const ZigVTable = std.mem.Allocator.VTable;
|
||||
|
||||
/// The VTable required by the C interface.
|
||||
/// C: GhosttyAllocatorVtable
|
||||
pub const VTable = extern struct {
|
||||
alloc: *const fn (*anyopaque, len: usize, alignment: u8, ret_addr: usize) callconv(.c) ?[*]u8,
|
||||
resize: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) bool,
|
||||
remap: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) ?[*]u8,
|
||||
free: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, ret_addr: usize) callconv(.c) void,
|
||||
};
|
||||
|
||||
/// Returns an allocator to use for the given possibly-null C allocator,
|
||||
/// ensuring some allocator is always returned.
|
||||
pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator {
|
||||
// If we're given an allocator, use it.
|
||||
if (c_alloc_) |c_alloc| return c_alloc.zig();
|
||||
|
||||
// If we have libc, use that. We prefer libc if we have it because
|
||||
// its generally fast but also lets the embedder easily override
|
||||
// malloc/free with custom allocators like mimalloc or something.
|
||||
if (comptime builtin.link_libc) return std.heap.c_allocator;
|
||||
|
||||
// No libc, use the preferred allocator for releases which is the
|
||||
// Zig SMP allocator.
|
||||
return std.heap.smp_allocator;
|
||||
}
|
||||
|
||||
/// The Allocator interface for custom memory allocation strategies
|
||||
/// within C libghostty APIs.
|
||||
///
|
||||
/// This -- purposely -- matches the Zig allocator interface. We do this
|
||||
/// for two reasons: (1) Zig's allocator interface is well proven in
|
||||
/// the real world to be flexible and useful, and (2) it allows us to
|
||||
/// easily convert C allocators to Zig allocators and vice versa, since
|
||||
/// we're written in Zig.
|
||||
///
|
||||
/// C: GhosttyAllocator
|
||||
pub const Allocator = extern struct {
|
||||
ctx: *anyopaque,
|
||||
vtable: *const VTable,
|
||||
|
||||
/// vtable for the Zig allocator interface to map our extern
|
||||
/// allocator to Zig's allocator interface.
|
||||
pub const zig_vtable: ZigVTable = .{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
};
|
||||
|
||||
/// Create a C allocator from a Zig allocator. This requires that
|
||||
/// the Zig allocator be pointer-stable for the lifetime of the
|
||||
/// C allocator.
|
||||
pub fn fromZig(zig_alloc: *const std.mem.Allocator) Allocator {
|
||||
return .{
|
||||
.ctx = @ptrCast(@constCast(zig_alloc)),
|
||||
.vtable = &ZigAllocator.vtable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a Zig allocator from this C allocator. This requires
|
||||
/// a pointer to a Zig allocator vtable that we can populate with
|
||||
/// our callbacks.
|
||||
pub fn zig(self: *const Allocator) std.mem.Allocator {
|
||||
return .{
|
||||
.ptr = @ptrCast(@constCast(self)),
|
||||
.vtable = &zig_vtable,
|
||||
};
|
||||
}
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
alignment: std.mem.Alignment,
|
||||
ra: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.alloc(
|
||||
self.ctx,
|
||||
len,
|
||||
@intFromEnum(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) bool {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.resize(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
return self.vtable.remap(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
old_mem: []u8,
|
||||
alignment: std.mem.Alignment,
|
||||
ra: usize,
|
||||
) void {
|
||||
const self: *Allocator = @ptrCast(@alignCast(ctx));
|
||||
self.vtable.free(
|
||||
self.ctx,
|
||||
old_mem.ptr,
|
||||
old_mem.len,
|
||||
@intFromEnum(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// An allocator implementation that wraps a Zig allocator so that
|
||||
/// it can be exposed to C.
|
||||
const ZigAllocator = struct {
|
||||
const vtable: VTable = .{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
};
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.alloc(
|
||||
zig_alloc.ptr,
|
||||
len,
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) bool {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.resize(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
new_len: usize,
|
||||
ra: usize,
|
||||
) callconv(.c) ?[*]u8 {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.remap(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
new_len,
|
||||
ra,
|
||||
);
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
memory: [*]u8,
|
||||
memory_len: usize,
|
||||
alignment: u8,
|
||||
ra: usize,
|
||||
) callconv(.c) void {
|
||||
const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx));
|
||||
return zig_alloc.vtable.free(
|
||||
zig_alloc.ptr,
|
||||
memory[0..memory_len],
|
||||
@enumFromInt(alignment),
|
||||
ra,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// libc Allocator, requires linking libc
|
||||
pub const c_allocator: Allocator = .fromZig(&std.heap.c_allocator);
|
||||
|
||||
/// Allocator that can be sent to the C API that does full
|
||||
/// leak checking within Zig tests. This should only be used from
|
||||
/// Zig tests.
|
||||
pub const test_allocator: Allocator = b: {
|
||||
if (!builtin.is_test) @compileError("test_allocator can only be used in tests");
|
||||
break :b .fromZig(&testing.allocator);
|
||||
};
|
||||
|
||||
test "c allocator" {
|
||||
if (!comptime builtin.link_libc) return error.SkipZigTest;
|
||||
|
||||
const alloc = c_allocator.zig();
|
||||
const str = try alloc.alloc(u8, 10);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqual(10, str.len);
|
||||
}
|
||||
|
||||
test "fba allocator" {
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fba: std.heap.FixedBufferAllocator = .init(&buf);
|
||||
const zig_alloc = fba.allocator();
|
||||
|
||||
// Convert the Zig allocator to a C interface
|
||||
const c_alloc: Allocator = .fromZig(&zig_alloc);
|
||||
|
||||
// Convert back to Zig so we can test it.
|
||||
const alloc = c_alloc.zig();
|
||||
const str = try alloc.alloc(u8, 10);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqual(10, str.len);
|
||||
}
|
||||
|
|
@ -65,6 +65,19 @@ pub const EraseLine = terminal.EraseLine;
|
|||
pub const TabClear = terminal.TabClear;
|
||||
pub const Attribute = terminal.Attribute;
|
||||
|
||||
comptime {
|
||||
// If we're building the C library (vs. the Zig module) then
|
||||
// we want to reference the C API so that it gets exported.
|
||||
if (terminal.is_c_lib) {
|
||||
const c = terminal.c_api;
|
||||
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
|
||||
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = terminal;
|
||||
|
||||
// Tests always test the C API
|
||||
_ = terminal.c_api;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const lib_alloc = @import("../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const osc = @import("osc.zig");
|
||||
|
||||
/// C: GhosttyOscParser
|
||||
pub const OscParser = *osc.Parser;
|
||||
|
||||
/// C: GhosttyResult
|
||||
pub const Result = enum(c_int) {
|
||||
success = 0,
|
||||
out_of_memory = -1,
|
||||
};
|
||||
|
||||
pub fn osc_new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *OscParser,
|
||||
) callconv(.c) Result {
|
||||
const alloc = lib_alloc.default(alloc_);
|
||||
const ptr = alloc.create(osc.Parser) catch
|
||||
return .out_of_memory;
|
||||
ptr.* = .initAlloc(alloc);
|
||||
result.* = ptr;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn osc_free(parser: OscParser) callconv(.c) void {
|
||||
// C-built parsers always have an associated allocator.
|
||||
const alloc = parser.alloc.?;
|
||||
parser.deinit();
|
||||
alloc.destroy(parser);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = lib_alloc;
|
||||
}
|
||||
|
||||
test "osc" {
|
||||
const testing = std.testing;
|
||||
var p: OscParser = undefined;
|
||||
try testing.expectEqual(Result.success, osc_new(
|
||||
&lib_alloc.test_allocator,
|
||||
&p,
|
||||
));
|
||||
osc_free(p);
|
||||
}
|
||||
|
|
@ -62,6 +62,10 @@ pub const Attribute = sgr.Attribute;
|
|||
|
||||
pub const isSafePaste = sanitize.isSafePaste;
|
||||
|
||||
/// This is set to true when we're building the C library.
|
||||
pub const is_c_lib = @import("root") == @import("../lib_vt.zig");
|
||||
pub const c_api = @import("c_api.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue