Merge pull request #3859 from laytan/wasm-stbtt-object-linking-preopens

wasm: support `vendor:stb/truetype` and `vendor:fontstash`
This commit is contained in:
gingerBill
2024-07-02 22:14:54 +01:00
committed by GitHub
15 changed files with 307 additions and 37 deletions

View File

@@ -26,7 +26,6 @@ O_CLOEXEC :: 0x80000
stdin: Handle = 0
stdout: Handle = 1
stderr: Handle = 2
current_dir: Handle = 3
args := _alloc_command_line_arguments()
@@ -38,6 +37,114 @@ _alloc_command_line_arguments :: proc() -> (args: []string) {
return
}
// WASI works with "preopened" directories, the environment retrieves directories
// (for example with `wasmtime --dir=. module.wasm`) and those given directories
// are the only ones accessible by the application.
//
// So in order to facilitate the `os` API (absolute paths etc.) we keep a list
// of the given directories and match them when needed (notably `os.open`).
@(private)
Preopen :: struct {
fd: wasi.fd_t,
prefix: string,
}
@(private)
preopens: []Preopen
@(init, private)
init_preopens :: proc() {
strip_prefixes :: proc(path: string) -> string {
path := path
loop: for len(path) > 0 {
switch {
case path[0] == '/':
path = path[1:]
case len(path) > 2 && path[0] == '.' && path[1] == '/':
path = path[2:]
case len(path) == 1 && path[0] == '.':
path = path[1:]
case:
break loop
}
}
return path
}
dyn_preopens: [dynamic]Preopen
loop: for fd := wasi.fd_t(3); ; fd += 1 {
desc, err := wasi.fd_prestat_get(fd)
#partial switch err {
case .BADF: break loop
case: panic("fd_prestat_get returned an unexpected error")
case .SUCCESS:
}
switch desc.tag {
case .DIR:
buf := make([]byte, desc.dir.pr_name_len) or_else panic("could not allocate memory for filesystem preopens")
if err = wasi.fd_prestat_dir_name(fd, buf); err != .SUCCESS {
panic("could not get filesystem preopen dir name")
}
append(&dyn_preopens, Preopen{fd, strip_prefixes(string(buf))})
}
}
preopens = dyn_preopens[:]
}
wasi_match_preopen :: proc(path: string) -> (wasi.fd_t, string, bool) {
prefix_matches :: proc(prefix, path: string) -> bool {
// Empty is valid for any relative path.
if len(prefix) == 0 && len(path) > 0 && path[0] != '/' {
return true
}
if len(path) < len(prefix) {
return false
}
if path[:len(prefix)] != prefix {
return false
}
// Only match on full components.
i := len(prefix)
for i > 0 && prefix[i-1] == '/' {
i -= 1
}
return path[i] == '/'
}
path := path
for len(path) > 0 && path[0] == '/' {
path = path[1:]
}
match: Preopen
#reverse for preopen in preopens {
if (match.fd == 0 || len(preopen.prefix) > len(match.prefix)) && prefix_matches(preopen.prefix, path) {
match = preopen
}
}
if match.fd == 0 {
return 0, "", false
}
relative := path[len(match.prefix):]
for len(relative) > 0 && relative[0] == '/' {
relative = relative[1:]
}
if len(relative) == 0 {
relative = "."
}
return match.fd, relative, true
}
write :: proc(fd: Handle, data: []byte) -> (int, Errno) {
iovs := wasi.ciovec_t(data)
n, err := wasi.fd_write(wasi.fd_t(fd), {iovs})
@@ -87,7 +194,13 @@ open :: proc(path: string, mode: int = O_RDONLY, perm: int = 0) -> (Handle, Errn
if mode & O_SYNC == O_SYNC {
fdflags += {.SYNC}
}
fd, err := wasi.path_open(wasi.fd_t(current_dir),{.SYMLINK_FOLLOW},path,oflags,rights,{},fdflags)
dir_fd, relative, ok := wasi_match_preopen(path)
if !ok {
return INVALID_HANDLE, Errno(wasi.errno_t.BADF)
}
fd, err := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, oflags, rights, {}, fdflags)
return Handle(fd), Errno(err)
}
close :: proc(fd: Handle) -> Errno {

View File

@@ -962,7 +962,7 @@ prestat_dir_t :: struct {
}
prestat_t :: struct {
tag: u8,
tag: preopentype_t,
using u: struct {
dir: prestat_dir_t,
},
@@ -1158,7 +1158,7 @@ foreign wasi {
/**
* A buffer into which to write the preopened directory name.
*/
path: string,
path: []byte,
) -> errno_t ---
/**
* Create a directory.

View File

@@ -860,15 +860,6 @@ gb_internal bool is_arch_x86(void) {
return false;
}
gb_internal bool allow_check_foreign_filepath(void) {
switch (build_context.metrics.arch) {
case TargetArch_wasm32:
case TargetArch_wasm64p32:
return false;
}
return true;
}
// TODO(bill): OS dependent versions for the BuildContext
// join_path
// is_dir

View File

@@ -1178,9 +1178,12 @@ gb_internal void check_proc_decl(CheckerContext *ctx, Entity *e, DeclInfo *d) {
if (foreign_library->LibraryName.paths.count >= 1) {
module_name = foreign_library->LibraryName.paths[0];
}
name = concatenate3_strings(permanent_allocator(), module_name, WASM_MODULE_NAME_SEPARATOR, name);
if (!string_ends_with(module_name, str_lit(".o"))) {
name = concatenate3_strings(permanent_allocator(), module_name, WASM_MODULE_NAME_SEPARATOR, name);
}
}
e->Procedure.is_foreign = true;
e->Procedure.link_name = name;

View File

@@ -5000,9 +5000,8 @@ gb_internal void check_foreign_import_fullpaths(Checker *c) {
String file_str = op.value.value_string;
file_str = string_trim_whitespace(file_str);
String fullpath = file_str;
if (allow_check_foreign_filepath()) {
if (!is_arch_wasm() || string_ends_with(file_str, str_lit(".o"))) {
String foreign_path = {};
bool ok = determine_path_from_string(nullptr, decl, base_dir, file_str, &foreign_path, /*use error not syntax_error*/true);
if (ok) {

View File

@@ -85,6 +85,20 @@ gb_internal i32 linker_stage(LinkerData *gen) {
if (extra_linker_flags.len != 0) {
lib_str = gb_string_append_fmt(lib_str, " %.*s", LIT(extra_linker_flags));
}
for_array(i, e->LibraryName.paths) {
String lib = e->LibraryName.paths[i];
if (lib.len == 0) {
continue;
}
if (!string_ends_with(lib, str_lit(".o"))) {
continue;
}
inputs = gb_string_append_fmt(inputs, " \"%.*s\"", LIT(lib));
}
}
if (build_context.metrics.os == TargetOs_orca) {

View File

@@ -2029,7 +2029,11 @@ gb_internal void lb_set_wasm_procedure_import_attributes(LLVMValueRef value, Ent
GB_ASSERT(foreign_library->LibraryName.paths.count == 1);
module_name = foreign_library->LibraryName.paths[0];
if (string_ends_with(module_name, str_lit(".o"))) {
return;
}
if (string_starts_with(import_name, module_name)) {
import_name = substring(import_name, module_name.len+WASM_MODULE_NAME_SEPARATOR.len, import_name.len);
}

View File

@@ -5824,7 +5824,6 @@ gb_internal bool determine_path_from_string(BlockingMutex *file_mutex, Ast *node
return false;
}
if (collection_name.len > 0) {
// NOTE(bill): `base:runtime` == `core:runtime`
if (collection_name == "core") {
@@ -5979,7 +5978,7 @@ gb_internal void parse_setup_file_decls(Parser *p, AstFile *f, String const &bas
Token fp_token = fp->BasicLit.token;
String file_str = string_trim_whitespace(string_value_from_token(f, fp_token));
String fullpath = file_str;
if (allow_check_foreign_filepath()) {
if (!is_arch_wasm() || string_ends_with(fullpath, str_lit(".o"))) {
String foreign_path = {};
bool ok = determine_path_from_string(&p->file_decl_mutex, node, base_dir, file_str, &foreign_path);
if (!ok) {

View File

@@ -1,14 +1,14 @@
//+build windows, linux, darwin
//+vet !using-param
package fontstash
import "base:runtime"
import "core:log"
import "core:os"
import "core:mem"
import "core:math"
import "core:strings"
import "core:slice"
import stbtt "vendor:stb/truetype"
// This is a port from Fontstash into odin - specialized for nanovg
@@ -321,20 +321,6 @@ __AtlasAddWhiteRect :: proc(ctx: ^FontContext, w, h: int) -> bool {
return true
}
AddFontPath :: proc(
ctx: ^FontContext,
name: string,
path: string,
) -> int {
data, ok := os.read_entire_file(path)
if !ok {
log.panicf("FONT: failed to read font at %s", path)
}
return AddFontMem(ctx, name, data, true)
}
// push a font to the font stack
// optionally init with ascii characters at a wanted size
AddFontMem :: proc(
@@ -1192,4 +1178,4 @@ EndState :: proc(using ctx: ^FontContext) {
}
__dirtyRectReset(ctx)
}
}
}

20
vendor/fontstash/fontstash_os.odin vendored Normal file
View File

@@ -0,0 +1,20 @@
//+build !js
package fontstash
import "core:log"
import "core:os"
AddFontPath :: proc(
ctx: ^FontContext,
name: string,
path: string,
) -> int {
data, ok := os.read_entire_file(path)
if !ok {
log.panicf("FONT: failed to read font at %s", path)
}
return AddFontMem(ctx, name, data, true)
}

BIN
vendor/stb/lib/stb_truetype_wasm.o vendored Normal file

Binary file not shown.

View File

@@ -6,6 +6,10 @@ else
all: unix
endif
wasm:
mkdir -p ../lib
clang -c -Os --target=wasm32 -nostdlib stb_truetype_wasm.c -o ../lib/stb_truetype_wasm.o
unix:
mkdir -p ../lib
$(CC) -c -O2 -Os -fPIC stb_image.c stb_image_write.c stb_image_resize.c stb_truetype.c stb_rect_pack.c stb_vorbis.c

46
vendor/stb/src/stb_truetype_wasm.c vendored Normal file
View File

@@ -0,0 +1,46 @@
#include <stddef.h>
void *stbtt_malloc(size_t size);
void stbtt_free(void *ptr);
void stbtt_qsort(void* base, size_t num, size_t size, int (*compare)(const void*, const void*));
double stbtt_floor(double x);
double stbtt_ceil(double x);
double stbtt_sqrt(double x);
double stbtt_pow(double x, double y);
double stbtt_fmod(double x, double y);
double stbtt_cos(double x);
double stbtt_acos(double x);
double stbtt_fabs(double x);
unsigned long stbtt_strlen(const char *str);
void *memcpy(void *dst, const void *src, size_t count);
void *memset(void *dst, int x, size_t count);
#define STBRP_SORT stbtt_qsort
#define STBRP_ASSERT(condition) ((void)0)
#define STBTT_malloc(x,u) ((void)(u),stbtt_malloc(x))
#define STBTT_free(x,u) ((void)(u),stbtt_free(x))
#define STBTT_assert(condition) ((void)0)
#define STBTT_ifloor(x) ((int) stbtt_floor(x))
#define STBTT_iceil(x) ((int) stbtt_ceil(x))
#define STBTT_sqrt(x) stbtt_sqrt(x)
#define STBTT_pow(x,y) stbtt_pow(x,y)
#define STBTT_fmod(x,y) stbtt_fmod(x,y)
#define STBTT_cos(x) stbtt_cos(x)
#define STBTT_acos(x) stbtt_acos(x)
#define STBTT_fabs(x) stbtt_fabs(x)
#define STBTT_strlen(x) stbtt_strlen(x)
#define STBTT_memcpy memcpy
#define STBTT_memset memset
#define STB_RECT_PACK_IMPLEMENTATION
#include "stb_rect_pack.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"

View File

@@ -17,6 +17,8 @@ when LIB != "" {
}
foreign import stbtt { LIB }
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
foreign import stbtt "../lib/stb_truetype_wasm.o"
} else {
foreign import stbtt "system:stb_truetype"
}

View File

@@ -0,0 +1,89 @@
//+build wasm32, wasm64p32
package stb_truetype
import "base:builtin"
import "base:intrinsics"
import "base:runtime"
import "core:c"
import "core:math"
import "core:mem"
import "core:slice"
import "core:sort"
@(require, linkage="strong", link_name="stbtt_malloc")
malloc :: proc "c" (size: uint) -> rawptr {
context = runtime.default_context()
ptr, _ := runtime.mem_alloc_non_zeroed(int(size))
return raw_data(ptr)
}
@(require, linkage="strong", link_name="stbtt_free")
free :: proc "c" (ptr: rawptr) {
context = runtime.default_context()
builtin.free(ptr)
}
@(require, linkage="strong", link_name="stbtt_qsort")
qsort :: proc "c" (base: rawptr, num: uint, size: uint, cmp: proc "c" (a, b: rawptr) -> i32) {
context = runtime.default_context()
Inputs :: struct {
base: rawptr,
num: uint,
size: uint,
cmp: proc "c" (a, b: rawptr) -> i32,
}
sort.sort({
collection = &Inputs{base, num, size, cmp},
len = proc(it: sort.Interface) -> int {
inputs := (^Inputs)(it.collection)
return int(inputs.num)
},
less = proc(it: sort.Interface, i, j: int) -> bool {
inputs := (^Inputs)(it.collection)
a := rawptr(uintptr(inputs.base) + (uintptr(i) * uintptr(inputs.size)))
b := rawptr(uintptr(inputs.base) + (uintptr(j) * uintptr(inputs.size)))
return inputs.cmp(a, b) < 0
},
swap = proc(it: sort.Interface, i, j: int) {
inputs := (^Inputs)(it.collection)
a := rawptr(uintptr(inputs.base) + (uintptr(i) * uintptr(inputs.size)))
b := rawptr(uintptr(inputs.base) + (uintptr(j) * uintptr(inputs.size)))
slice.ptr_swap_non_overlapping(a, b, int(inputs.size))
},
})
}
@(require, linkage="strong", link_name="stbtt_floor")
floor :: proc "c" (x: f64) -> f64 { return math.floor(x) }
@(require, linkage="strong", link_name="stbtt_ceil")
ceil :: proc "c" (x: f64) -> f64 { return math.ceil(x) }
@(require, linkage="strong", link_name="stbtt_sqrt")
sqrt :: proc "c" (x: f64) -> f64 { return math.sqrt(x) }
@(require, linkage="strong", link_name="stbtt_pow")
pow :: proc "c" (x, y: f64) -> f64 { return math.pow(x, y) }
@(require, linkage="strong", link_name="stbtt_fmod")
fmod :: proc "c" (x, y: f64) -> f64 {
context = runtime.default_context();
// NOTE: only called in the `stbtt_GetGlyphSDF` code path.
panic("`math.round` is broken on 32 bit targets, see #3856")
}
@(require, linkage="strong", link_name="stbtt_cos")
cos :: proc "c" (x: f64) -> f64 { return math.cos(x) }
@(require, linkage="strong", link_name="stbtt_acos")
acos :: proc "c" (x: f64) -> f64 { return math.acos(x) }
@(require, linkage="strong", link_name="stbtt_fabs")
fabs :: proc "c" (x: f64) -> f64 { return math.abs(x) }
@(require, linkage="strong", link_name="stbtt_strlen")
strlen :: proc "c" (str: cstring) -> c.ulong { return c.ulong(len(str)) }
// NOTE: defined in runtime.
// void *memcpy(void *dst, const void *src, size_t count);
// void *memset(void *dst, int x, size_t count);