Merge remote-tracking branch 'offical/master'

This commit is contained in:
ed
2025-01-30 14:36:46 -05:00
126 changed files with 2481 additions and 1004 deletions
+84 -3
View File
@@ -1,20 +1,101 @@
#+private
package os2
import "core:sys/linux"
Read_Directory_Iterator_Impl :: struct {
prev_fi: File_Info,
dirent_backing: []u8,
dirent_buflen: int,
dirent_off: int,
index: int,
}
@(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
scan_entries :: proc(dfd: linux.Fd, entries: []u8, offset: ^int) -> (fd: linux.Fd, file_name: string) {
for d in linux.dirent_iterate_buf(entries, offset) {
file_name = linux.dirent_name(d)
if file_name == "." || file_name == ".." {
continue
}
file_name_cstr := cstring(raw_data(file_name))
entry_fd, errno := linux.openat(dfd, file_name_cstr, {.NOFOLLOW, .PATH})
if errno == .NONE {
return entry_fd, file_name
}
}
return -1, ""
}
index = it.impl.index
it.impl.index += 1
dfd := linux.Fd(_fd(it.f))
entries := it.impl.dirent_backing[:it.impl.dirent_buflen]
entry_fd, file_name := scan_entries(dfd, entries, &it.impl.dirent_off)
for entry_fd == -1 {
if len(it.impl.dirent_backing) == 0 {
it.impl.dirent_backing = make([]u8, 512, file_allocator())
}
loop: for {
buflen, errno := linux.getdents(linux.Fd(dfd), it.impl.dirent_backing[:])
#partial switch errno {
case .EINVAL:
delete(it.impl.dirent_backing, file_allocator())
n := len(it.impl.dirent_backing) * 2
it.impl.dirent_backing = make([]u8, n, file_allocator())
continue
case .NONE:
if buflen == 0 {
return
}
it.impl.dirent_off = 0
it.impl.dirent_buflen = buflen
entries = it.impl.dirent_backing[:buflen]
break loop
case: // error
return
}
}
entry_fd, file_name = scan_entries(dfd, entries, &it.impl.dirent_off)
}
defer linux.close(entry_fd)
file_info_delete(it.impl.prev_fi, file_allocator())
fi, _ = _fstat_internal(entry_fd, file_allocator())
it.impl.prev_fi = fi
ok = true
return
}
@(require_results)
_read_directory_iterator_create :: proc(f: ^File) -> (Read_Directory_Iterator, Error) {
return {}, .Unsupported
if f == nil || f.impl == nil {
return {}, .Invalid_File
}
stat: linux.Stat
errno := linux.fstat(linux.Fd(fd(f)), &stat)
if errno != .NONE {
return {}, _get_platform_error(errno)
}
if (stat.mode & linux.S_IFMT) != linux.S_IFDIR {
return {}, .Invalid_Dir
}
return {f = f}, nil
}
_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
if it == nil {
return
}
delete(it.impl.dirent_backing, file_allocator())
file_info_delete(it.impl.prev_fi, file_allocator())
}
+6 -3
View File
@@ -39,8 +39,11 @@ _read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info
}
n := len(fimpl.name)+1
non_zero_resize(&it.impl.fullpath, n+len(sname))
n += copy(it.impl.fullpath[n:], sname)
if err := non_zero_resize(&it.impl.fullpath, n+len(sname)); err != nil {
// Can't really tell caller we had an error, sad.
return
}
copy(it.impl.fullpath[n:], sname)
fi = internal_stat(stat, string(it.impl.fullpath[:]))
ok = true
@@ -60,7 +63,7 @@ _read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Itera
iter.f = f
iter.impl.idx = 0
iter.impl.fullpath.allocator = file_allocator()
iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
append(&iter.impl.fullpath, impl.name)
append(&iter.impl.fullpath, "/")
defer if err != nil { delete(iter.impl.fullpath) }
+110
View File
@@ -0,0 +1,110 @@
#+private
package os2
import "base:intrinsics"
import "core:sys/wasm/wasi"
Read_Directory_Iterator_Impl :: struct {
fullpath: [dynamic]byte,
buf: []byte,
off: int,
idx: int,
}
@(require_results)
_read_directory_iterator :: proc(it: ^Read_Directory_Iterator) -> (fi: File_Info, index: int, ok: bool) {
fimpl := (^File_Impl)(it.f.impl)
buf := it.impl.buf[it.impl.off:]
index = it.impl.idx
it.impl.idx += 1
for {
if len(buf) < size_of(wasi.dirent_t) {
return
}
entry := intrinsics.unaligned_load((^wasi.dirent_t)(raw_data(buf)))
buf = buf[size_of(wasi.dirent_t):]
if len(buf) < int(entry.d_namlen) {
// shouldn't be possible.
return
}
name := string(buf[:entry.d_namlen])
buf = buf[entry.d_namlen:]
it.impl.off += size_of(wasi.dirent_t) + int(entry.d_namlen)
if name == "." || name == ".." {
continue
}
n := len(fimpl.name)+1
if alloc_err := non_zero_resize(&it.impl.fullpath, n+len(name)); alloc_err != nil {
// Can't really tell caller we had an error, sad.
return
}
copy(it.impl.fullpath[n:], name)
stat, err := wasi.path_filestat_get(__fd(it.f), {}, name)
if err != nil {
// Can't stat, fill what we have from dirent.
stat = {
ino = entry.d_ino,
filetype = entry.d_type,
}
}
fi = internal_stat(stat, string(it.impl.fullpath[:]))
ok = true
return
}
}
@(require_results)
_read_directory_iterator_create :: proc(f: ^File) -> (iter: Read_Directory_Iterator, err: Error) {
if f == nil || f.impl == nil {
err = .Invalid_File
return
}
impl := (^File_Impl)(f.impl)
iter.f = f
buf: [dynamic]byte
buf.allocator = file_allocator()
defer if err != nil { delete(buf) }
// NOTE: this is very grug.
for {
non_zero_resize(&buf, 512 if len(buf) == 0 else len(buf)*2) or_return
n, _err := wasi.fd_readdir(__fd(f), buf[:], 0)
if _err != nil {
err = _get_platform_error(_err)
return
}
if n < len(buf) {
non_zero_resize(&buf, n)
break
}
assert(n == len(buf))
}
iter.impl.buf = buf[:]
iter.impl.fullpath = make([dynamic]byte, 0, len(impl.name)+128, file_allocator()) or_return
append(&iter.impl.fullpath, impl.name)
append(&iter.impl.fullpath, "/")
return
}
_read_directory_iterator_destroy :: proc(it: ^Read_Directory_Iterator) {
delete(it.impl.buf, file_allocator())
delete(it.impl.fullpath)
it^ = {}
}
+5 -5
View File
@@ -76,7 +76,7 @@ _set_env :: proc(key, v_new: string) -> bool {
// wasn't in the environment in the first place.
k_addr, v_addr := _kv_addr_from_val(v_curr, key)
if len(v_new) > len(v_curr) {
k_addr = ([^]u8)(heap_resize(k_addr, kv_size))
k_addr = ([^]u8)(runtime.heap_resize(k_addr, kv_size))
if k_addr == nil {
return false
}
@@ -90,7 +90,7 @@ _set_env :: proc(key, v_new: string) -> bool {
}
}
k_addr := ([^]u8)(heap_alloc(kv_size))
k_addr := ([^]u8)(runtime.heap_alloc(kv_size))
if k_addr == nil {
return false
}
@@ -129,7 +129,7 @@ _unset_env :: proc(key: string) -> bool {
// if we got this far, the envrionment variable
// existed AND was allocated by us.
k_addr, _ := _kv_addr_from_val(v, key)
heap_free(k_addr)
runtime.heap_free(k_addr)
return true
}
@@ -139,7 +139,7 @@ _clear_env :: proc() {
for kv in _env {
if !_is_in_org_env(kv) {
heap_free(raw_data(kv))
runtime.heap_free(raw_data(kv))
}
}
clear(&_env)
@@ -193,7 +193,7 @@ _build_env :: proc() {
return
}
_env = make(type_of(_env), heap_allocator())
_env = make(type_of(_env), runtime.heap_allocator())
cstring_env := _get_original_env()
_org_env_begin = uintptr(rawptr(cstring_env[0]))
for i := 0; cstring_env[i] != nil; i += 1 {
+186
View File
@@ -0,0 +1,186 @@
#+private
package os2
import "base:runtime"
import "core:strings"
import "core:sync"
import "core:sys/wasm/wasi"
g_env: map[string]string
g_env_buf: []byte
g_env_mutex: sync.RW_Mutex
g_env_error: Error
g_env_built: bool
build_env :: proc() -> (err: Error) {
if g_env_built || g_env_error != nil {
return g_env_error
}
sync.guard(&g_env_mutex)
if g_env_built || g_env_error != nil {
return g_env_error
}
defer if err != nil {
g_env_error = err
}
num_envs, size_of_envs, _err := wasi.environ_sizes_get()
if _err != nil {
return _get_platform_error(_err)
}
g_env = make(map[string]string, num_envs, file_allocator()) or_return
defer if err != nil { delete(g_env) }
g_env_buf = make([]byte, size_of_envs, file_allocator()) or_return
defer if err != nil { delete(g_env_buf, file_allocator()) }
TEMP_ALLOCATOR_GUARD()
envs := make([]cstring, num_envs, temp_allocator()) or_return
_err = wasi.environ_get(raw_data(envs), raw_data(g_env_buf))
if _err != nil {
return _get_platform_error(_err)
}
for env in envs {
key, _, value := strings.partition(string(env), "=")
g_env[key] = value
}
g_env_built = true
return
}
delete_string_if_not_original :: proc(str: string) {
start := uintptr(raw_data(g_env_buf))
end := start + uintptr(len(g_env_buf))
ptr := uintptr(raw_data(str))
if ptr < start || ptr > end {
delete(str, file_allocator())
}
}
@(require_results)
_lookup_env :: proc(key: string, allocator: runtime.Allocator) -> (value: string, found: bool) {
if err := build_env(); err != nil {
return
}
sync.shared_guard(&g_env_mutex)
value = g_env[key] or_return
value, _ = clone_string(value, allocator)
return
}
@(require_results)
_set_env :: proc(key, value: string) -> bool {
if err := build_env(); err != nil {
return false
}
sync.guard(&g_env_mutex)
key_ptr, value_ptr, just_inserted, err := map_entry(&g_env, key)
if err != nil {
return false
}
alloc_err: runtime.Allocator_Error
if just_inserted {
key_ptr^, alloc_err = clone_string(key, file_allocator())
if alloc_err != nil {
delete_key(&g_env, key)
return false
}
value_ptr^, alloc_err = clone_string(value, file_allocator())
if alloc_err != nil {
delete_key(&g_env, key)
delete(key_ptr^, file_allocator())
return false
}
return true
}
delete_string_if_not_original(value_ptr^)
value_ptr^, alloc_err = clone_string(value, file_allocator())
if alloc_err != nil {
delete_key(&g_env, key)
return false
}
return true
}
@(require_results)
_unset_env :: proc(key: string) -> bool {
if err := build_env(); err != nil {
return false
}
sync.guard(&g_env_mutex)
dkey, dval := delete_key(&g_env, key)
delete_string_if_not_original(dkey)
delete_string_if_not_original(dval)
return true
}
_clear_env :: proc() {
sync.guard(&g_env_mutex)
for k, v in g_env {
delete_string_if_not_original(k)
delete_string_if_not_original(v)
}
delete(g_env_buf, file_allocator())
g_env_buf = {}
clear(&g_env)
g_env_built = true
}
@(require_results)
_environ :: proc(allocator: runtime.Allocator) -> []string {
if err := build_env(); err != nil {
return nil
}
sync.shared_guard(&g_env_mutex)
envs, alloc_err := make([]string, len(g_env), allocator)
if alloc_err != nil {
return nil
}
defer if alloc_err != nil {
for env in envs {
delete(env, allocator)
}
delete(envs, allocator)
}
i: int
for k, v in g_env {
defer i += 1
envs[i], alloc_err = concatenate({k, "=", v}, allocator)
if alloc_err != nil {
return nil
}
}
return envs
}
+11 -2
View File
@@ -10,8 +10,12 @@ _error_string :: proc(errno: i32) -> string {
return string(posix.strerror(posix.Errno(errno)))
}
_get_platform_error :: proc() -> Error {
#partial switch errno := posix.errno(); errno {
_get_platform_error_from_errno :: proc() -> Error {
return _get_platform_error_existing(posix.errno())
}
_get_platform_error_existing :: proc(errno: posix.Errno) -> Error {
#partial switch errno {
case .EPERM:
return .Permission_Denied
case .EEXIST:
@@ -32,3 +36,8 @@ _get_platform_error :: proc() -> Error {
return Platform_Error(errno)
}
}
_get_platform_error :: proc{
_get_platform_error_existing,
_get_platform_error_from_errno,
}
+47
View File
@@ -0,0 +1,47 @@
#+private
package os2
import "base:runtime"
import "core:slice"
import "core:sys/wasm/wasi"
_Platform_Error :: wasi.errno_t
_error_string :: proc(errno: i32) -> string {
e := wasi.errno_t(errno)
if e == .NONE {
return ""
}
err := runtime.Type_Info_Enum_Value(e)
ti := &runtime.type_info_base(type_info_of(wasi.errno_t)).variant.(runtime.Type_Info_Enum)
if idx, ok := slice.binary_search(ti.values, err); ok {
return ti.names[idx]
}
return "<unknown platform error>"
}
_get_platform_error :: proc(errno: wasi.errno_t) -> Error {
#partial switch errno {
case .PERM:
return .Permission_Denied
case .EXIST:
return .Exist
case .NOENT:
return .Not_Exist
case .TIMEDOUT:
return .Timeout
case .PIPE:
return .Broken_Pipe
case .BADF:
return .Invalid_File
case .NOMEM:
return .Out_Of_Memory
case .NOSYS:
return .Unsupported
case:
return Platform_Error(errno)
}
}
+41 -20
View File
@@ -7,6 +7,13 @@ import "core:time"
import "core:sync"
import "core:sys/linux"
// Most implementations will EINVAL at some point when doing big writes.
// In practice a read/write call would probably never read/write these big buffers all at once,
// which is why the number of bytes is returned and why there are procs that will call this in a
// loop for you.
// We set a max of 1GB to keep alignment and to be safe.
MAX_RW :: 1 << 30
File_Impl :: struct {
file: File,
name: string,
@@ -179,10 +186,11 @@ _seek :: proc(f: ^File_Impl, offset: i64, whence: io.Seek_From) -> (ret: i64, er
}
_read :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
if len(p) == 0 {
if len(p) <= 0 {
return 0, nil
}
n, errno := linux.read(f.fd, p[:])
n, errno := linux.read(f.fd, p[:min(len(p), MAX_RW)])
if errno != .NONE {
return -1, _get_platform_error(errno)
}
@@ -190,13 +198,13 @@ _read :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
}
_read_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
if len(p) == 0 {
if len(p) <= 0 {
return 0, nil
}
if offset < 0 {
return 0, .Invalid_Offset
}
n, errno := linux.pread(f.fd, p[:], offset)
n, errno := linux.pread(f.fd, p[:min(len(p), MAX_RW)], offset)
if errno != .NONE {
return -1, _get_platform_error(errno)
}
@@ -206,29 +214,42 @@ _read_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
return i64(n), nil
}
_write :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
if len(p) == 0 {
return 0, nil
_write :: proc(f: ^File_Impl, p: []byte) -> (nt: i64, err: Error) {
p := p
for len(p) > 0 {
n, errno := linux.write(f.fd, p[:min(len(p), MAX_RW)])
if errno != .NONE {
err = _get_platform_error(errno)
return
}
p = p[n:]
nt += i64(n)
}
n, errno := linux.write(f.fd, p[:])
if errno != .NONE {
return -1, _get_platform_error(errno)
}
return i64(n), nil
return
}
_write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
if len(p) == 0 {
return 0, nil
}
_write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (nt: i64, err: Error) {
if offset < 0 {
return 0, .Invalid_Offset
}
n, errno := linux.pwrite(f.fd, p[:], offset)
if errno != .NONE {
return -1, _get_platform_error(errno)
p := p
offset := offset
for len(p) > 0 {
n, errno := linux.pwrite(f.fd, p[:min(len(p), MAX_RW)], offset)
if errno != .NONE {
err = _get_platform_error(errno)
return
}
p = p[n:]
nt += i64(n)
offset += i64(n)
}
return i64(n), nil
return
}
_file_size :: proc(f: ^File_Impl) -> (n: i64, err: Error) {
+534
View File
@@ -0,0 +1,534 @@
#+private
package os2
import "base:runtime"
import "core:io"
import "core:sys/wasm/wasi"
import "core:time"
// NOTE: Don't know if there is a max in wasi.
MAX_RW :: 1 << 30
File_Impl :: struct {
file: File,
name: string,
fd: wasi.fd_t,
allocator: runtime.Allocator,
}
// 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`).
Preopen :: struct {
fd: wasi.fd_t,
prefix: string,
}
preopens: []Preopen
@(init)
init_std_files :: proc() {
new_std :: proc(impl: ^File_Impl, fd: wasi.fd_t, name: string) -> ^File {
impl.file.impl = impl
impl.allocator = runtime.nil_allocator()
impl.fd = fd
impl.name = string(name)
impl.file.stream = {
data = impl,
procedure = _file_stream_proc,
}
impl.file.fstat = _fstat
return &impl.file
}
@(static) files: [3]File_Impl
stdin = new_std(&files[0], 0, "/dev/stdin")
stdout = new_std(&files[1], 1, "/dev/stdout")
stderr = new_std(&files[2], 2, "/dev/stderr")
}
@(init)
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
}
n: int
n_loop: for fd := wasi.fd_t(3); ; fd += 1 {
_, err := wasi.fd_prestat_get(fd)
#partial switch err {
case .BADF: break n_loop
case .SUCCESS: n += 1
case:
print_error(stderr, _get_platform_error(err), "unexpected error from wasi_prestat_get")
break n_loop
}
}
alloc_err: runtime.Allocator_Error
preopens, alloc_err = make([]Preopen, n, file_allocator())
if alloc_err != nil {
print_error(stderr, alloc_err, "could not allocate memory for wasi preopens")
return
}
loop: for &preopen, i in preopens {
fd := wasi.fd_t(3 + i)
desc, err := wasi.fd_prestat_get(fd)
assert(err == .SUCCESS)
switch desc.tag {
case .DIR:
buf: []byte
buf, alloc_err = make([]byte, desc.dir.pr_name_len, file_allocator())
if alloc_err != nil {
print_error(stderr, alloc_err, "could not allocate memory for wasi preopen dir name")
continue loop
}
if err = wasi.fd_prestat_dir_name(fd, buf); err != .SUCCESS {
print_error(stderr, _get_platform_error(err), "could not get filesystem preopen dir name")
continue loop
}
preopen.fd = fd
preopen.prefix = strip_prefixes(string(buf))
}
}
}
@(require_results)
match_preopen :: proc(path: string) -> (wasi.fd_t, string, bool) {
@(require_results)
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
if path == "" {
return 0, "", false
}
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
}
_open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Error) {
dir_fd, relative, ok := match_preopen(name)
if !ok {
return nil, .Invalid_Path
}
oflags: wasi.oflags_t
if .Create in flags { oflags += {.CREATE} }
if .Excl in flags { oflags += {.EXCL} }
if .Trunc in flags { oflags += {.TRUNC} }
fdflags: wasi.fdflags_t
if .Append in flags { fdflags += {.APPEND} }
if .Sync in flags { fdflags += {.SYNC} }
// NOTE: rights are adjusted to what this package's functions might want to call.
rights: wasi.rights_t
if .Read in flags { rights += {.FD_READ, .FD_FILESTAT_GET, .PATH_FILESTAT_GET} }
if .Write in flags { rights += {.FD_WRITE, .FD_SYNC, .FD_FILESTAT_SET_SIZE, .FD_FILESTAT_SET_TIMES, .FD_SEEK} }
fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, oflags, rights, {}, fdflags)
if fderr != nil {
err = _get_platform_error(fderr)
return
}
return _new_file(uintptr(fd), name, file_allocator())
}
_new_file :: proc(handle: uintptr, name: string, allocator: runtime.Allocator) -> (f: ^File, err: Error) {
if name == "" {
err = .Invalid_Path
return
}
impl := new(File_Impl, allocator) or_return
defer if err != nil { free(impl, allocator) }
impl.allocator = allocator
// NOTE: wasi doesn't really do full paths afact.
impl.name = clone_string(name, allocator) or_return
impl.fd = wasi.fd_t(handle)
impl.file.impl = impl
impl.file.stream = {
data = impl,
procedure = _file_stream_proc,
}
impl.file.fstat = _fstat
return &impl.file, nil
}
_close :: proc(f: ^File_Impl) -> (err: Error) {
if errno := wasi.fd_close(f.fd); errno != nil {
err = _get_platform_error(errno)
}
delete(f.name, f.allocator)
free(f, f.allocator)
return
}
_fd :: proc(f: ^File) -> uintptr {
return uintptr(__fd(f))
}
__fd :: proc(f: ^File) -> wasi.fd_t {
if f != nil && f.impl != nil {
return (^File_Impl)(f.impl).fd
}
return -1
}
_name :: proc(f: ^File) -> string {
if f != nil && f.impl != nil {
return (^File_Impl)(f.impl).name
}
return ""
}
_sync :: proc(f: ^File) -> Error {
return _get_platform_error(wasi.fd_sync(__fd(f)))
}
_truncate :: proc(f: ^File, size: i64) -> Error {
return _get_platform_error(wasi.fd_filestat_set_size(__fd(f), wasi.filesize_t(size)))
}
_remove :: proc(name: string) -> Error {
dir_fd, relative, ok := match_preopen(name)
if !ok {
return .Invalid_Path
}
err := wasi.path_remove_directory(dir_fd, relative)
if err == .NOTDIR {
err = wasi.path_unlink_file(dir_fd, relative)
}
return _get_platform_error(err)
}
_rename :: proc(old_path, new_path: string) -> Error {
src_dir_fd, src_relative, src_ok := match_preopen(old_path)
if !src_ok {
return .Invalid_Path
}
new_dir_fd, new_relative, new_ok := match_preopen(new_path)
if !new_ok {
return .Invalid_Path
}
return _get_platform_error(wasi.path_rename(src_dir_fd, src_relative, new_dir_fd, new_relative))
}
_link :: proc(old_name, new_name: string) -> Error {
src_dir_fd, src_relative, src_ok := match_preopen(old_name)
if !src_ok {
return .Invalid_Path
}
new_dir_fd, new_relative, new_ok := match_preopen(new_name)
if !new_ok {
return .Invalid_Path
}
return _get_platform_error(wasi.path_link(src_dir_fd, {.SYMLINK_FOLLOW}, src_relative, new_dir_fd, new_relative))
}
_symlink :: proc(old_name, new_name: string) -> Error {
src_dir_fd, src_relative, src_ok := match_preopen(old_name)
if !src_ok {
return .Invalid_Path
}
new_dir_fd, new_relative, new_ok := match_preopen(new_name)
if !new_ok {
return .Invalid_Path
}
if src_dir_fd != new_dir_fd {
return .Invalid_Path
}
return _get_platform_error(wasi.path_symlink(src_relative, src_dir_fd, new_relative))
}
_read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, err: Error) {
dir_fd, relative, ok := match_preopen(name)
if !ok {
return "", .Invalid_Path
}
n, _err := wasi.path_readlink(dir_fd, relative, nil)
if _err != nil {
err = _get_platform_error(_err)
return
}
buf := make([]byte, n, allocator) or_return
_, _err = wasi.path_readlink(dir_fd, relative, buf)
s = string(buf)
err = _get_platform_error(_err)
return
}
_chdir :: proc(name: string) -> Error {
return .Unsupported
}
_fchdir :: proc(f: ^File) -> Error {
return .Unsupported
}
_fchmod :: proc(f: ^File, mode: int) -> Error {
return .Unsupported
}
_chmod :: proc(name: string, mode: int) -> Error {
return .Unsupported
}
_fchown :: proc(f: ^File, uid, gid: int) -> Error {
return .Unsupported
}
_chown :: proc(name: string, uid, gid: int) -> Error {
return .Unsupported
}
_lchown :: proc(name: string, uid, gid: int) -> Error {
return .Unsupported
}
_chtimes :: proc(name: string, atime, mtime: time.Time) -> Error {
dir_fd, relative, ok := match_preopen(name)
if !ok {
return .Invalid_Path
}
_atime := wasi.timestamp_t(atime._nsec)
_mtime := wasi.timestamp_t(mtime._nsec)
return _get_platform_error(wasi.path_filestat_set_times(dir_fd, {.SYMLINK_FOLLOW}, relative, _atime, _mtime, {.MTIM, .ATIM}))
}
_fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error {
_atime := wasi.timestamp_t(atime._nsec)
_mtime := wasi.timestamp_t(mtime._nsec)
return _get_platform_error(wasi.fd_filestat_set_times(__fd(f), _atime, _mtime, {.ATIM, .MTIM}))
}
_exists :: proc(path: string) -> bool {
dir_fd, relative, ok := match_preopen(path)
if !ok {
return false
}
_, err := wasi.path_filestat_get(dir_fd, {.SYMLINK_FOLLOW}, relative)
if err != nil {
return false
}
return true
}
_file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte, offset: i64, whence: io.Seek_From) -> (n: i64, err: io.Error) {
f := (^File_Impl)(stream_data)
fd := f.fd
switch mode {
case .Read:
if len(p) <= 0 {
return
}
to_read := min(len(p), MAX_RW)
_n, _err := wasi.fd_read(fd, {p[:to_read]})
n = i64(_n)
if _err != nil {
err = .Unknown
} else if n == 0 {
err = .EOF
}
return
case .Read_At:
if len(p) <= 0 {
return
}
if offset < 0 {
err = .Invalid_Offset
return
}
to_read := min(len(p), MAX_RW)
_n, _err := wasi.fd_pread(fd, {p[:to_read]}, wasi.filesize_t(offset))
n = i64(_n)
if _err != nil {
err = .Unknown
} else if n == 0 {
err = .EOF
}
return
case .Write:
p := p
for len(p) > 0 {
to_write := min(len(p), MAX_RW)
_n, _err := wasi.fd_write(fd, {p[:to_write]})
if _err != nil {
err = .Unknown
return
}
p = p[_n:]
n += i64(_n)
}
return
case .Write_At:
p := p
offset := offset
if offset < 0 {
err = .Invalid_Offset
return
}
for len(p) > 0 {
to_write := min(len(p), MAX_RW)
_n, _err := wasi.fd_pwrite(fd, {p[:to_write]}, wasi.filesize_t(offset))
if _err != nil {
err = .Unknown
return
}
p = p[_n:]
n += i64(_n)
offset += i64(_n)
}
return
case .Seek:
#assert(int(wasi.whence_t.SET) == int(io.Seek_From.Start))
#assert(int(wasi.whence_t.CUR) == int(io.Seek_From.Current))
#assert(int(wasi.whence_t.END) == int(io.Seek_From.End))
switch whence {
case .Start, .Current, .End:
break
case:
err = .Invalid_Whence
return
}
_n, _err := wasi.fd_seek(fd, wasi.filedelta_t(offset), wasi.whence_t(whence))
#partial switch _err {
case .INVAL:
err = .Invalid_Offset
case:
err = .Unknown
case .SUCCESS:
n = i64(_n)
}
return
case .Size:
stat, _err := wasi.fd_filestat_get(fd)
if _err != nil {
err = .Unknown
return
}
n = i64(stat.size)
return
case .Flush:
ferr := _sync(&f.file)
err = error_to_io_error(ferr)
return
case .Close, .Destroy:
ferr := _close(f)
err = error_to_io_error(ferr)
return
case .Query:
return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Destroy, .Query})
case:
return 0, .Empty
}
}
+2 -722
View File
@@ -1,726 +1,6 @@
#+private
package os2
import "core:sys/linux"
import "core:sync"
import "core:mem"
// NOTEs
//
// All allocations below DIRECT_MMAP_THRESHOLD exist inside of memory "Regions." A region
// consists of a Region_Header and the memory that will be divided into allocations to
// send to the user. The memory is an array of "Allocation_Headers" which are 8 bytes.
// Allocation_Headers are used to navigate the memory in the region. The "next" member of
// the Allocation_Header points to the next header, and the space between the headers
// can be used to send to the user. This space between is referred to as "blocks" in the
// code. The indexes in the header refer to these blocks instead of bytes. This allows us
// to index all the memory in the region with a u16.
//
// When an allocation request is made, it will use the first free block that can contain
// the entire block. If there is an excess number of blocks (as specified by the constant
// BLOCK_SEGMENT_THRESHOLD), this extra space will be segmented and left in the free_list.
//
// To keep the implementation simple, there can never exist 2 free blocks adjacent to each
// other. Any freeing will result in attempting to merge the blocks before and after the
// newly free'd blocks.
//
// Any request for size above the DIRECT_MMAP_THRESHOLD will result in the allocation
// getting its own individual mmap. Individual mmaps will still get an Allocation_Header
// that contains the size with the last bit set to 1 to indicate it is indeed a direct
// mmap allocation.
// Why not brk?
// glibc's malloc utilizes a mix of the brk and mmap system calls. This implementation
// does *not* utilize the brk system call to avoid possible conflicts with foreign C
// code. Just because we aren't directly using libc, there is nothing stopping the user
// from doing it.
// What's with all the #no_bounds_check?
// When memory is returned from mmap, it technically doesn't get written ... well ... anywhere
// until that region is written to by *you*. So, when a new region is created, we call mmap
// to get a pointer to some memory, and we claim that memory is a ^Region. Therefor, the
// region itself is never formally initialized by the compiler as this would result in writing
// zeros to memory that we can already assume are 0. This would also have the effect of
// actually commiting this data to memory whether it gets used or not.
//
// Some variables to play with
//
// Minimum blocks used for any one allocation
MINIMUM_BLOCK_COUNT :: 2
// Number of extra blocks beyond the requested amount where we would segment.
// E.g. (blocks) |H0123456| 7 available
// |H01H0123| Ask for 2, now 4 available
BLOCK_SEGMENT_THRESHOLD :: 4
// Anything above this threshold will get its own memory map. Since regions
// are indexed by 16 bit integers, this value should not surpass max(u16) * 6
DIRECT_MMAP_THRESHOLD_USER :: int(max(u16))
// The point at which we convert direct mmap to region. This should be a decent
// amount less than DIRECT_MMAP_THRESHOLD to avoid jumping in and out of regions.
MMAP_TO_REGION_SHRINK_THRESHOLD :: DIRECT_MMAP_THRESHOLD - PAGE_SIZE * 4
// free_list is dynamic and is initialized in the begining of the region memory
// when the region is initialized. Once resized, it can be moved anywhere.
FREE_LIST_DEFAULT_CAP :: 32
//
// Other constants that should not be touched
//
// This universally seems to be 4096 outside of uncommon archs.
PAGE_SIZE :: 4096
// just rounding up to nearest PAGE_SIZE
DIRECT_MMAP_THRESHOLD :: (DIRECT_MMAP_THRESHOLD_USER-1) + PAGE_SIZE - (DIRECT_MMAP_THRESHOLD_USER-1) % PAGE_SIZE
// Regions must be big enough to hold DIRECT_MMAP_THRESHOLD - 1 as well
// as end right on a page boundary as to not waste space.
SIZE_OF_REGION :: DIRECT_MMAP_THRESHOLD + 4 * int(PAGE_SIZE)
// size of user memory blocks
BLOCK_SIZE :: size_of(Allocation_Header)
// number of allocation sections (call them blocks) of the region used for allocations
BLOCKS_PER_REGION :: u16((SIZE_OF_REGION - size_of(Region_Header)) / BLOCK_SIZE)
// minimum amount of space that can used by any individual allocation (includes header)
MINIMUM_ALLOCATION :: (MINIMUM_BLOCK_COUNT * BLOCK_SIZE) + BLOCK_SIZE
// This is used as a boolean value for Region_Header.local_addr.
CURRENTLY_ACTIVE :: (^^Region)(~uintptr(0))
FREE_LIST_ENTRIES_PER_BLOCK :: BLOCK_SIZE / size_of(u16)
MMAP_FLAGS : linux.Map_Flags : {.ANONYMOUS, .PRIVATE}
MMAP_PROT : linux.Mem_Protection : {.READ, .WRITE}
@thread_local _local_region: ^Region
global_regions: ^Region
// There is no way of correctly setting the last bit of free_idx or
// the last bit of requested, so we can safely use it as a flag to
// determine if we are interacting with a direct mmap.
REQUESTED_MASK :: 0x7FFFFFFFFFFFFFFF
IS_DIRECT_MMAP :: 0x8000000000000000
// Special free_idx value that does not index the free_list.
NOT_FREE :: 0x7FFF
Allocation_Header :: struct #raw_union {
using _: struct {
// Block indicies
idx: u16,
prev: u16,
next: u16,
free_idx: u16,
},
requested: u64,
}
Region_Header :: struct #align(16) {
next_region: ^Region, // points to next region in global_heap (linked list)
local_addr: ^^Region, // tracks region ownership via address of _local_region
reset_addr: ^^Region, // tracks old local addr for reset
free_list: []u16,
free_list_len: u16,
free_blocks: u16, // number of free blocks in region (includes headers)
last_used: u16, // farthest back block that has been used (need zeroing?)
_reserved: u16,
}
Region :: struct {
hdr: Region_Header,
memory: [BLOCKS_PER_REGION]Allocation_Header,
}
_heap_allocator_proc :: proc(allocator_data: rawptr, mode: mem.Allocator_Mode,
size, alignment: int,
old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, mem.Allocator_Error) {
//
// NOTE(tetra, 2020-01-14): The heap doesn't respect alignment.
// Instead, we overallocate by `alignment + size_of(rawptr) - 1`, and insert
// padding. We also store the original pointer returned by heap_alloc right before
// the pointer we return to the user.
//
aligned_alloc :: proc(size, alignment: int, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) {
a := max(alignment, align_of(rawptr))
space := size + a - 1
allocated_mem: rawptr
if old_ptr != nil {
original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
allocated_mem = heap_resize(original_old_ptr, space+size_of(rawptr))
} else {
allocated_mem = heap_alloc(space+size_of(rawptr))
}
aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))
ptr := uintptr(aligned_mem)
aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
diff := int(aligned_ptr - ptr)
if (size + diff) > space || allocated_mem == nil {
return nil, .Out_Of_Memory
}
aligned_mem = rawptr(aligned_ptr)
mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem
return mem.byte_slice(aligned_mem, size), nil
}
aligned_free :: proc(p: rawptr) {
if p != nil {
heap_free(mem.ptr_offset((^rawptr)(p), -1)^)
}
}
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> (new_memory: []byte, err: mem.Allocator_Error) {
if p == nil {
return nil, nil
}
return aligned_alloc(new_size, new_alignment, p)
}
switch mode {
case .Alloc, .Alloc_Non_Zeroed:
return aligned_alloc(size, alignment)
case .Free:
aligned_free(old_memory)
case .Free_All:
return nil, .Mode_Not_Implemented
case .Resize, .Resize_Non_Zeroed:
if old_memory == nil {
return aligned_alloc(size, alignment)
}
return aligned_resize(old_memory, old_size, size, alignment)
case .Query_Features:
set := (^mem.Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Free, .Resize, .Query_Features}
}
return nil, nil
case .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, nil
}
heap_alloc :: proc(size: int) -> rawptr {
if size >= DIRECT_MMAP_THRESHOLD {
return _direct_mmap_alloc(size)
}
// atomically check if the local region has been stolen
if _local_region != nil {
res := sync.atomic_compare_exchange_strong_explicit(
&_local_region.hdr.local_addr,
&_local_region,
CURRENTLY_ACTIVE,
.Acquire,
.Relaxed,
)
if res != &_local_region {
// At this point, the region has been stolen and res contains the unexpected value
expected := res
if res != CURRENTLY_ACTIVE {
expected = res
res = sync.atomic_compare_exchange_strong_explicit(
&_local_region.hdr.local_addr,
expected,
CURRENTLY_ACTIVE,
.Acquire,
.Relaxed,
)
}
if res != expected {
_local_region = nil
}
}
}
size := size
size = _round_up_to_nearest(size, BLOCK_SIZE)
blocks_needed := u16(max(MINIMUM_BLOCK_COUNT, size / BLOCK_SIZE))
// retrieve a region if new thread or stolen
if _local_region == nil {
_local_region, _ = _region_retrieve_with_space(blocks_needed)
if _local_region == nil {
return nil
}
}
defer sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
// At this point we have a usable region. Let's find the user some memory
idx: u16
local_region_idx := _region_get_local_idx()
back_idx := -1
infinite: for {
for i := 0; i < int(_local_region.hdr.free_list_len); i += 1 {
idx = _local_region.hdr.free_list[i]
#no_bounds_check if _get_block_count(_local_region.memory[idx]) >= blocks_needed {
break infinite
}
}
sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
_local_region, back_idx = _region_retrieve_with_space(blocks_needed, local_region_idx, back_idx)
}
user_ptr, used := _region_get_block(_local_region, idx, blocks_needed)
sync.atomic_sub_explicit(&_local_region.hdr.free_blocks, used + 1, .Release)
// If this memory was ever used before, it now needs to be zero'd.
if idx < _local_region.hdr.last_used {
mem.zero(user_ptr, int(used) * BLOCK_SIZE)
} else {
_local_region.hdr.last_used = idx + used
}
return user_ptr
}
heap_resize :: proc(old_memory: rawptr, new_size: int) -> rawptr #no_bounds_check {
alloc := _get_allocation_header(old_memory)
if alloc.requested & IS_DIRECT_MMAP > 0 {
return _direct_mmap_resize(alloc, new_size)
}
if new_size > DIRECT_MMAP_THRESHOLD {
return _direct_mmap_from_region(alloc, new_size)
}
return _region_resize(alloc, new_size)
}
heap_free :: proc(memory: rawptr) {
alloc := _get_allocation_header(memory)
if sync.atomic_load(&alloc.requested) & IS_DIRECT_MMAP == IS_DIRECT_MMAP {
_direct_mmap_free(alloc)
return
}
assert(alloc.free_idx == NOT_FREE)
_region_find_and_assign_local(alloc)
_region_local_free(alloc)
sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
}
//
// Regions
//
_new_region :: proc() -> ^Region #no_bounds_check {
ptr, errno := linux.mmap(0, uint(SIZE_OF_REGION), MMAP_PROT, MMAP_FLAGS, -1, 0)
if errno != .NONE {
return nil
}
new_region := (^Region)(ptr)
new_region.hdr.local_addr = CURRENTLY_ACTIVE
new_region.hdr.reset_addr = &_local_region
free_list_blocks := _round_up_to_nearest(FREE_LIST_DEFAULT_CAP, FREE_LIST_ENTRIES_PER_BLOCK)
_region_assign_free_list(new_region, &new_region.memory[1], u16(free_list_blocks) * FREE_LIST_ENTRIES_PER_BLOCK)
// + 2 to account for free_list's allocation header
first_user_block := len(new_region.hdr.free_list) / FREE_LIST_ENTRIES_PER_BLOCK + 2
// first allocation header (this is a free list)
new_region.memory[0].next = u16(first_user_block)
new_region.memory[0].free_idx = NOT_FREE
new_region.memory[first_user_block].idx = u16(first_user_block)
new_region.memory[first_user_block].next = BLOCKS_PER_REGION - 1
// add the first user block to the free list
new_region.hdr.free_list[0] = u16(first_user_block)
new_region.hdr.free_list_len = 1
new_region.hdr.free_blocks = _get_block_count(new_region.memory[first_user_block]) + 1
for r := sync.atomic_compare_exchange_strong(&global_regions, nil, new_region);
r != nil;
r = sync.atomic_compare_exchange_strong(&r.hdr.next_region, nil, new_region) {}
return new_region
}
_region_resize :: proc(alloc: ^Allocation_Header, new_size: int, alloc_is_free_list: bool = false) -> rawptr #no_bounds_check {
assert(alloc.free_idx == NOT_FREE)
old_memory := mem.ptr_offset(alloc, 1)
old_block_count := _get_block_count(alloc^)
new_block_count := u16(
max(MINIMUM_BLOCK_COUNT, _round_up_to_nearest(new_size, BLOCK_SIZE) / BLOCK_SIZE),
)
if new_block_count < old_block_count {
if new_block_count - old_block_count >= MINIMUM_BLOCK_COUNT {
_region_find_and_assign_local(alloc)
_region_segment(_local_region, alloc, new_block_count, alloc.free_idx)
new_block_count = _get_block_count(alloc^)
sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
}
// need to zero anything within the new block that that lies beyond new_size
extra_bytes := int(new_block_count * BLOCK_SIZE) - new_size
extra_bytes_ptr := mem.ptr_offset((^u8)(alloc), new_size + BLOCK_SIZE)
mem.zero(extra_bytes_ptr, extra_bytes)
return old_memory
}
if !alloc_is_free_list {
_region_find_and_assign_local(alloc)
}
defer if !alloc_is_free_list {
sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
}
// First, let's see if we can grow in place.
if alloc.next != BLOCKS_PER_REGION - 1 && _local_region.memory[alloc.next].free_idx != NOT_FREE {
next_alloc := _local_region.memory[alloc.next]
total_available := old_block_count + _get_block_count(next_alloc) + 1
if total_available >= new_block_count {
alloc.next = next_alloc.next
_local_region.memory[alloc.next].prev = alloc.idx
if total_available - new_block_count > BLOCK_SEGMENT_THRESHOLD {
_region_segment(_local_region, alloc, new_block_count, next_alloc.free_idx)
} else {
_region_free_list_remove(_local_region, next_alloc.free_idx)
}
mem.zero(&_local_region.memory[next_alloc.idx], int(alloc.next - next_alloc.idx) * BLOCK_SIZE)
_local_region.hdr.last_used = max(alloc.next, _local_region.hdr.last_used)
_local_region.hdr.free_blocks -= (_get_block_count(alloc^) - old_block_count)
if alloc_is_free_list {
_region_assign_free_list(_local_region, old_memory, _get_block_count(alloc^))
}
return old_memory
}
}
// If we made it this far, we need to resize, copy, zero and free.
region_iter := _local_region
local_region_idx := _region_get_local_idx()
back_idx := -1
idx: u16
infinite: for {
for i := 0; i < int(region_iter.hdr.free_list_len); i += 1 {
idx = region_iter.hdr.free_list[i]
if _get_block_count(region_iter.memory[idx]) >= new_block_count {
break infinite
}
}
if region_iter != _local_region {
sync.atomic_store_explicit(
&region_iter.hdr.local_addr,
region_iter.hdr.reset_addr,
.Release,
)
}
region_iter, back_idx = _region_retrieve_with_space(new_block_count, local_region_idx, back_idx)
}
if region_iter != _local_region {
sync.atomic_store_explicit(
&region_iter.hdr.local_addr,
region_iter.hdr.reset_addr,
.Release,
)
}
// copy from old memory
new_memory, used_blocks := _region_get_block(region_iter, idx, new_block_count)
mem.copy(new_memory, old_memory, int(old_block_count * BLOCK_SIZE))
// zero any new memory
addon_section := mem.ptr_offset((^Allocation_Header)(new_memory), old_block_count)
new_blocks := used_blocks - old_block_count
mem.zero(addon_section, int(new_blocks) * BLOCK_SIZE)
region_iter.hdr.free_blocks -= (used_blocks + 1)
// Set free_list before freeing.
if alloc_is_free_list {
_region_assign_free_list(_local_region, new_memory, used_blocks)
}
// free old memory
_region_local_free(alloc)
return new_memory
}
_region_local_free :: proc(alloc: ^Allocation_Header) #no_bounds_check {
alloc := alloc
add_to_free_list := true
idx := sync.atomic_load(&alloc.idx)
prev := sync.atomic_load(&alloc.prev)
next := sync.atomic_load(&alloc.next)
block_count := next - idx - 1
free_blocks := sync.atomic_load(&_local_region.hdr.free_blocks) + block_count + 1
sync.atomic_store_explicit(&_local_region.hdr.free_blocks, free_blocks, .Release)
// try to merge with prev
if idx > 0 && sync.atomic_load(&_local_region.memory[prev].free_idx) != NOT_FREE {
sync.atomic_store_explicit(&_local_region.memory[prev].next, next, .Release)
_local_region.memory[next].prev = prev
alloc = &_local_region.memory[prev]
add_to_free_list = false
}
// try to merge with next
if next < BLOCKS_PER_REGION - 1 && sync.atomic_load(&_local_region.memory[next].free_idx) != NOT_FREE {
old_next := next
sync.atomic_store_explicit(&alloc.next, sync.atomic_load(&_local_region.memory[old_next].next), .Release)
sync.atomic_store_explicit(&_local_region.memory[next].prev, idx, .Release)
if add_to_free_list {
sync.atomic_store_explicit(&_local_region.hdr.free_list[_local_region.memory[old_next].free_idx], idx, .Release)
sync.atomic_store_explicit(&alloc.free_idx, _local_region.memory[old_next].free_idx, .Release)
} else {
// NOTE: We have aleady merged with prev, and now merged with next.
// Now, we are actually going to remove from the free_list.
_region_free_list_remove(_local_region, _local_region.memory[old_next].free_idx)
}
add_to_free_list = false
}
// This is the only place where anything is appended to the free list.
if add_to_free_list {
fl := _local_region.hdr.free_list
fl_len := sync.atomic_load(&_local_region.hdr.free_list_len)
sync.atomic_store_explicit(&alloc.free_idx, fl_len, .Release)
fl[alloc.free_idx] = idx
sync.atomic_store_explicit(&_local_region.hdr.free_list_len, fl_len + 1, .Release)
if int(fl_len + 1) == len(fl) {
free_alloc := _get_allocation_header(mem.raw_data(_local_region.hdr.free_list))
_region_resize(free_alloc, len(fl) * 2 * size_of(fl[0]), true)
}
}
}
_region_assign_free_list :: proc(region: ^Region, memory: rawptr, blocks: u16) {
raw_free_list := transmute(mem.Raw_Slice)region.hdr.free_list
raw_free_list.len = int(blocks) * FREE_LIST_ENTRIES_PER_BLOCK
raw_free_list.data = memory
region.hdr.free_list = transmute([]u16)(raw_free_list)
}
_region_retrieve_with_space :: proc(blocks: u16, local_idx: int = -1, back_idx: int = -1) -> (^Region, int) {
r: ^Region
idx: int
for r = sync.atomic_load(&global_regions); r != nil; r = r.hdr.next_region {
if idx == local_idx || idx < back_idx || sync.atomic_load(&r.hdr.free_blocks) < blocks {
idx += 1
continue
}
idx += 1
local_addr: ^^Region = sync.atomic_load(&r.hdr.local_addr)
if local_addr != CURRENTLY_ACTIVE {
res := sync.atomic_compare_exchange_strong_explicit(
&r.hdr.local_addr,
local_addr,
CURRENTLY_ACTIVE,
.Acquire,
.Relaxed,
)
if res == local_addr {
r.hdr.reset_addr = local_addr
return r, idx
}
}
}
return _new_region(), idx
}
_region_retrieve_from_addr :: proc(addr: rawptr) -> ^Region {
r: ^Region
for r = global_regions; r != nil; r = r.hdr.next_region {
if _region_contains_mem(r, addr) {
return r
}
}
unreachable()
}
_region_get_block :: proc(region: ^Region, idx, blocks_needed: u16) -> (rawptr, u16) #no_bounds_check {
alloc := &region.memory[idx]
assert(alloc.free_idx != NOT_FREE)
assert(alloc.next > 0)
block_count := _get_block_count(alloc^)
if block_count - blocks_needed > BLOCK_SEGMENT_THRESHOLD {
_region_segment(region, alloc, blocks_needed, alloc.free_idx)
} else {
_region_free_list_remove(region, alloc.free_idx)
}
alloc.free_idx = NOT_FREE
return mem.ptr_offset(alloc, 1), _get_block_count(alloc^)
}
_region_segment :: proc(region: ^Region, alloc: ^Allocation_Header, blocks, new_free_idx: u16) #no_bounds_check {
old_next := alloc.next
alloc.next = alloc.idx + blocks + 1
region.memory[old_next].prev = alloc.next
// Initialize alloc.next allocation header here.
region.memory[alloc.next].prev = alloc.idx
region.memory[alloc.next].next = old_next
region.memory[alloc.next].idx = alloc.next
region.memory[alloc.next].free_idx = new_free_idx
// Replace our original spot in the free_list with new segment.
region.hdr.free_list[new_free_idx] = alloc.next
}
_region_get_local_idx :: proc() -> int {
idx: int
for r := sync.atomic_load(&global_regions); r != nil; r = r.hdr.next_region {
if r == _local_region {
return idx
}
idx += 1
}
return -1
}
_region_find_and_assign_local :: proc(alloc: ^Allocation_Header) {
// Find the region that contains this memory
if !_region_contains_mem(_local_region, alloc) {
_local_region = _region_retrieve_from_addr(alloc)
}
// At this point, _local_region is set correctly. Spin until acquire
res := CURRENTLY_ACTIVE
for res == CURRENTLY_ACTIVE {
res = sync.atomic_compare_exchange_strong_explicit(
&_local_region.hdr.local_addr,
&_local_region,
CURRENTLY_ACTIVE,
.Acquire,
.Relaxed,
)
}
}
_region_contains_mem :: proc(r: ^Region, memory: rawptr) -> bool #no_bounds_check {
if r == nil {
return false
}
mem_int := uintptr(memory)
return mem_int >= uintptr(&r.memory[0]) && mem_int <= uintptr(&r.memory[BLOCKS_PER_REGION - 1])
}
_region_free_list_remove :: proc(region: ^Region, free_idx: u16) #no_bounds_check {
// pop, swap and update allocation hdr
if n := region.hdr.free_list_len - 1; free_idx != n {
region.hdr.free_list[free_idx] = sync.atomic_load(&region.hdr.free_list[n])
alloc_idx := region.hdr.free_list[free_idx]
sync.atomic_store_explicit(&region.memory[alloc_idx].free_idx, free_idx, .Release)
}
region.hdr.free_list_len -= 1
}
//
// Direct mmap
//
_direct_mmap_alloc :: proc(size: int) -> rawptr {
mmap_size := _round_up_to_nearest(size + BLOCK_SIZE, PAGE_SIZE)
new_allocation, errno := linux.mmap(0, uint(mmap_size), MMAP_PROT, MMAP_FLAGS, -1, 0)
if errno != .NONE {
return nil
}
alloc := (^Allocation_Header)(uintptr(new_allocation))
alloc.requested = u64(size) // NOTE: requested = requested size
alloc.requested += IS_DIRECT_MMAP
return rawptr(mem.ptr_offset(alloc, 1))
}
_direct_mmap_resize :: proc(alloc: ^Allocation_Header, new_size: int) -> rawptr {
old_requested := int(alloc.requested & REQUESTED_MASK)
old_mmap_size := _round_up_to_nearest(old_requested + BLOCK_SIZE, PAGE_SIZE)
new_mmap_size := _round_up_to_nearest(new_size + BLOCK_SIZE, PAGE_SIZE)
if int(new_mmap_size) < MMAP_TO_REGION_SHRINK_THRESHOLD {
return _direct_mmap_to_region(alloc, new_size)
} else if old_requested == new_size {
return mem.ptr_offset(alloc, 1)
}
new_allocation, errno := linux.mremap(alloc, uint(old_mmap_size), uint(new_mmap_size), {.MAYMOVE})
if errno != .NONE {
return nil
}
new_header := (^Allocation_Header)(uintptr(new_allocation))
new_header.requested = u64(new_size)
new_header.requested += IS_DIRECT_MMAP
if new_mmap_size > old_mmap_size {
// new section may not be pointer aligned, so cast to ^u8
new_section := mem.ptr_offset((^u8)(new_header), old_requested + BLOCK_SIZE)
mem.zero(new_section, new_mmap_size - old_mmap_size)
}
return mem.ptr_offset(new_header, 1)
}
_direct_mmap_from_region :: proc(alloc: ^Allocation_Header, new_size: int) -> rawptr {
new_memory := _direct_mmap_alloc(new_size)
if new_memory != nil {
old_memory := mem.ptr_offset(alloc, 1)
mem.copy(new_memory, old_memory, int(_get_block_count(alloc^)) * BLOCK_SIZE)
}
_region_find_and_assign_local(alloc)
_region_local_free(alloc)
sync.atomic_store_explicit(&_local_region.hdr.local_addr, &_local_region, .Release)
return new_memory
}
_direct_mmap_to_region :: proc(alloc: ^Allocation_Header, new_size: int) -> rawptr {
new_memory := heap_alloc(new_size)
if new_memory != nil {
mem.copy(new_memory, mem.ptr_offset(alloc, -1), new_size)
_direct_mmap_free(alloc)
}
return new_memory
}
_direct_mmap_free :: proc(alloc: ^Allocation_Header) {
requested := int(alloc.requested & REQUESTED_MASK)
mmap_size := _round_up_to_nearest(requested + BLOCK_SIZE, PAGE_SIZE)
linux.munmap(alloc, uint(mmap_size))
}
//
// Util
//
_get_block_count :: #force_inline proc(alloc: Allocation_Header) -> u16 {
return alloc.next - alloc.idx - 1
}
_get_allocation_header :: #force_inline proc(raw_mem: rawptr) -> ^Allocation_Header {
return mem.ptr_offset((^Allocation_Header)(raw_mem), -1)
}
_round_up_to_nearest :: #force_inline proc(size, round: int) -> int {
return (size-1) + round - (size-1) % round
}
import "base:runtime"
_heap_allocator_proc :: runtime.heap_allocator_proc
+6
View File
@@ -0,0 +1,6 @@
#+private
package os2
import "base:runtime"
_heap_allocator_proc :: runtime.wasm_allocator_proc
+12
View File
@@ -2,6 +2,8 @@ package os2
import "base:runtime"
import "core:path/filepath"
Path_Separator :: _Path_Separator // OS-Specific
Path_Separator_String :: _Path_Separator_String // OS-Specific
Path_List_Separator :: _Path_List_Separator // OS-Specific
@@ -39,3 +41,13 @@ setwd :: set_working_directory
set_working_directory :: proc(dir: string) -> (err: Error) {
return _set_working_directory(dir)
}
get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
return _get_executable_path(allocator)
}
get_executable_directory :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
path = _get_executable_path(allocator) or_return
path, _ = filepath.split(path)
return
}
+17
View File
@@ -0,0 +1,17 @@
package os2
import "base:runtime"
import "core:sys/darwin"
import "core:sys/posix"
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
buffer: [darwin.PIDPATHINFO_MAXSIZE]byte = ---
ret := darwin.proc_pidpath(posix.getpid(), raw_data(buffer[:]), len(buffer))
if ret > 0 {
return clone_string(string(buffer[:ret]), allocator)
}
err = _get_platform_error()
return
}
+29
View File
@@ -0,0 +1,29 @@
package os2
import "base:runtime"
import "core:sys/freebsd"
import "core:sys/posix"
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
req := []freebsd.MIB_Identifier{.CTL_KERN, .KERN_PROC, .KERN_PROC_PATHNAME, freebsd.MIB_Identifier(-1)}
size: uint
if ret := freebsd.sysctl(req, nil, &size, nil, 0); ret != .NONE {
err = _get_platform_error(posix.Errno(ret))
return
}
assert(size > 0)
buf := make([]byte, size, allocator) or_return
defer if err != nil { delete(buf, allocator) }
assert(uint(len(buf)) == size)
if ret := freebsd.sysctl(req, raw_data(buf), &size, nil, 0); ret != .NONE {
err = _get_platform_error(posix.Errno(ret))
return
}
return string(buf[:size]), nil
}
+21 -1
View File
@@ -1,9 +1,10 @@
#+private
package os2
import "base:runtime"
import "core:strings"
import "core:strconv"
import "base:runtime"
import "core:sys/linux"
_Path_Separator :: '/'
@@ -171,6 +172,25 @@ _set_working_directory :: proc(dir: string) -> Error {
return _get_platform_error(linux.chdir(dir_cstr))
}
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
TEMP_ALLOCATOR_GUARD()
buf := make([dynamic]byte, 1024, temp_allocator()) or_return
for {
n, errno := linux.readlink("/proc/self/exe", buf[:])
if errno != .NONE {
err = _get_platform_error(errno)
return
}
if n < len(buf) {
return clone_string(string(buf[:n]), allocator)
}
resize(&buf, len(buf)*2) or_return
}
}
_get_full_path :: proc(fd: linux.Fd, allocator: runtime.Allocator) -> (fullpath: string, err: Error) {
PROC_FD_PATH :: "/proc/self/fd/"
+24
View File
@@ -0,0 +1,24 @@
package os2
import "base:runtime"
import "core:sys/posix"
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
TEMP_ALLOCATOR_GUARD()
buf := make([dynamic]byte, 1024, temp_allocator()) or_return
for {
n := posix.readlink("/proc/curproc/exe", raw_data(buf), len(buf))
if n < 0 {
err = _get_platform_error()
return
}
if n < len(buf) {
return clone_string(string(buf[:n]), allocator)
}
resize(&buf, len(buf)*2) or_return
}
}
+57
View File
@@ -0,0 +1,57 @@
package os2
import "base:runtime"
import "core:strings"
import "core:sys/posix"
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
// OpenBSD does not have an API for this, we do our best below.
if len(runtime.args__) <= 0 {
err = .Invalid_Path
return
}
real :: proc(path: cstring, allocator: runtime.Allocator) -> (out: string, err: Error) {
real := posix.realpath(path)
if real == nil {
err = _get_platform_error()
return
}
defer posix.free(real)
return clone_string(string(real), allocator)
}
arg := runtime.args__[0]
sarg := string(arg)
if len(sarg) == 0 {
err = .Invalid_Path
return
}
if sarg[0] == '.' || sarg[0] == '/' {
return real(arg, allocator)
}
TEMP_ALLOCATOR_GUARD()
buf := strings.builder_make(temp_allocator())
paths := get_env("PATH", temp_allocator())
for dir in strings.split_iterator(&paths, ":") {
strings.builder_reset(&buf)
strings.write_string(&buf, dir)
strings.write_string(&buf, "/")
strings.write_string(&buf, sarg)
cpath := strings.to_cstring(&buf)
if posix.access(cpath, {.X_OK}) == .OK {
return real(cpath, allocator)
}
}
err = .Invalid_Path
return
}
+1 -1
View File
@@ -81,7 +81,7 @@ _remove_all :: proc(path: string) -> Error {
fullpath, _ := concatenate({path, "/", string(cname), "\x00"}, temp_allocator())
if entry.d_type == .DIR {
_remove_all(fullpath[:len(fullpath)-1])
_remove_all(fullpath[:len(fullpath)-1]) or_return
} else {
if posix.unlink(cstring(raw_data(fullpath))) != .OK {
return _get_platform_error()
+113
View File
@@ -0,0 +1,113 @@
#+private
package os2
import "base:runtime"
import "core:path/filepath"
import "core:sync"
import "core:sys/wasm/wasi"
_Path_Separator :: '/'
_Path_Separator_String :: "/"
_Path_List_Separator :: ':'
_is_path_separator :: proc(c: byte) -> bool {
return c == _Path_Separator
}
_mkdir :: proc(name: string, perm: int) -> Error {
dir_fd, relative, ok := match_preopen(name)
if !ok {
return .Invalid_Path
}
return _get_platform_error(wasi.path_create_directory(dir_fd, relative))
}
_mkdir_all :: proc(path: string, perm: int) -> Error {
if path == "" {
return .Invalid_Path
}
TEMP_ALLOCATOR_GUARD()
if exists(path) {
return .Exist
}
clean_path := filepath.clean(path, temp_allocator())
return internal_mkdir_all(clean_path)
internal_mkdir_all :: proc(path: string) -> Error {
dir, file := filepath.split(path)
if file != path && dir != "/" {
if len(dir) > 1 && dir[len(dir) - 1] == '/' {
dir = dir[:len(dir) - 1]
}
internal_mkdir_all(dir) or_return
}
err := _mkdir(path, 0)
if err == .Exist { err = nil }
return err
}
}
_remove_all :: proc(path: string) -> (err: Error) {
// PERF: this works, but wastes a bunch of memory using the read_directory_iterator API
// and using open instead of wasi fds directly.
{
dir := open(path) or_return
defer close(dir)
iter := read_directory_iterator_create(dir) or_return
defer read_directory_iterator_destroy(&iter)
for fi in read_directory_iterator(&iter) {
if fi.type == .Directory {
_remove_all(fi.fullpath) or_return
} else {
remove(fi.fullpath) or_return
}
}
}
return remove(path)
}
g_wd: string
g_wd_mutex: sync.Mutex
_get_working_directory :: proc(allocator: runtime.Allocator) -> (dir: string, err: Error) {
sync.guard(&g_wd_mutex)
return clone_string(g_wd if g_wd != "" else "/", allocator)
}
_set_working_directory :: proc(dir: string) -> (err: Error) {
sync.guard(&g_wd_mutex)
if dir == g_wd {
return
}
if g_wd != "" {
delete(g_wd, file_allocator())
}
g_wd = clone_string(dir, file_allocator()) or_return
return
}
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
if len(args) <= 0 {
return clone_string("/", allocator)
}
arg := args[0]
if len(arg) > 0 && (arg[0] == '.' || arg[0] == '/') {
return clone_string(arg, allocator)
}
return concatenate({"/", arg}, allocator)
}
+20
View File
@@ -136,6 +136,26 @@ _set_working_directory :: proc(dir: string) -> (err: Error) {
return
}
_get_executable_path :: proc(allocator: runtime.Allocator) -> (path: string, err: Error) {
TEMP_ALLOCATOR_GUARD()
buf := make([dynamic]u16, 512, temp_allocator()) or_return
for {
ret := win32.GetModuleFileNameW(nil, raw_data(buf), win32.DWORD(len(buf)))
if ret == 0 {
err = _get_platform_error()
return
}
if ret == win32.DWORD(len(buf)) && win32.GetLastError() == win32.ERROR_INSUFFICIENT_BUFFER {
resize(&buf, len(buf)*2) or_return
continue
}
return win32_utf16_to_utf8(buf[:ret], allocator)
}
}
can_use_long_paths: bool
@(init)
+13
View File
@@ -0,0 +1,13 @@
#+private
package os2
_pipe :: proc() -> (r, w: ^File, err: Error) {
err = .Unsupported
return
}
@(require_results)
_pipe_has_data :: proc(r: ^File) -> (ok: bool, err: Error) {
err = .Unsupported
return
}
+5
View File
@@ -547,6 +547,11 @@ _process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
if _, errno = linux.dup2(stderr_fd, STDERR); errno != .NONE {
write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
}
if dir_fd != linux.AT_FDCWD {
if errno = linux.fchdir(dir_fd); errno != .NONE {
write_errno_to_parent_and_abort(child_pipe_fds[WRITE], errno)
}
}
errno = linux.execveat(dir_fd, exe_path, &cargs[0], env)
assert(errno != nil)
+89
View File
@@ -0,0 +1,89 @@
#+private
package os2
import "base:runtime"
import "core:time"
import "core:sys/wasm/wasi"
_exit :: proc "contextless" (code: int) -> ! {
wasi.proc_exit(wasi.exitcode_t(code))
}
_get_uid :: proc() -> int {
return 0
}
_get_euid :: proc() -> int {
return 0
}
_get_gid :: proc() -> int {
return 0
}
_get_egid :: proc() -> int {
return 0
}
_get_pid :: proc() -> int {
return 0
}
_get_ppid :: proc() -> int {
return 0
}
_process_info_by_handle :: proc(process: Process, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
err = .Unsupported
return
}
_current_process_info :: proc(selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
err = .Unsupported
return
}
_Sys_Process_Attributes :: struct {}
_process_start :: proc(desc: Process_Desc) -> (process: Process, err: Error) {
err = .Unsupported
return
}
_process_wait :: proc(process: Process, timeout: time.Duration) -> (process_state: Process_State, err: Error) {
err = .Unsupported
return
}
_process_close :: proc(process: Process) -> Error {
return .Unsupported
}
_process_kill :: proc(process: Process) -> (err: Error) {
return .Unsupported
}
_process_info_by_pid :: proc(pid: int, selection: Process_Info_Fields, allocator: runtime.Allocator) -> (info: Process_Info, err: Error) {
err = .Unsupported
return
}
_process_list :: proc(allocator: runtime.Allocator) -> (list: []int, err: Error) {
err = .Unsupported
return
}
_process_open :: proc(pid: int, flags: Process_Open_Flags) -> (process: Process, err: Error) {
process.pid = pid
err = .Unsupported
return
}
_process_handle_still_valid :: proc(p: Process) -> Error {
return nil
}
_process_state_update_times :: proc(p: Process, state: ^Process_State) {
return
}
+101
View File
@@ -0,0 +1,101 @@
#+private
package os2
import "base:runtime"
import "core:path/filepath"
import "core:sys/wasm/wasi"
import "core:time"
internal_stat :: proc(stat: wasi.filestat_t, fullpath: string) -> (fi: File_Info) {
fi.fullpath = fullpath
fi.name = filepath.base(fi.fullpath)
fi.inode = u128(stat.ino)
fi.size = i64(stat.size)
switch stat.filetype {
case .BLOCK_DEVICE: fi.type = .Block_Device
case .CHARACTER_DEVICE: fi.type = .Character_Device
case .DIRECTORY: fi.type = .Directory
case .REGULAR_FILE: fi.type = .Regular
case .SOCKET_DGRAM, .SOCKET_STREAM: fi.type = .Socket
case .SYMBOLIC_LINK: fi.type = .Symlink
case .UNKNOWN: fi.type = .Undetermined
case: fi.type = .Undetermined
}
fi.creation_time = time.Time{_nsec=i64(stat.ctim)}
fi.modification_time = time.Time{_nsec=i64(stat.mtim)}
fi.access_time = time.Time{_nsec=i64(stat.atim)}
return
}
_fstat :: proc(f: ^File, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
if f == nil || f.impl == nil {
err = .Invalid_File
return
}
impl := (^File_Impl)(f.impl)
stat, _err := wasi.fd_filestat_get(__fd(f))
if _err != nil {
err = _get_platform_error(_err)
return
}
fullpath := clone_string(impl.name, allocator) or_return
return internal_stat(stat, fullpath), nil
}
_stat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
if name == "" {
err = .Invalid_Path
return
}
dir_fd, relative, ok := match_preopen(name)
if !ok {
err = .Invalid_Path
return
}
stat, _err := wasi.path_filestat_get(dir_fd, {.SYMLINK_FOLLOW}, relative)
if _err != nil {
err = _get_platform_error(_err)
return
}
// NOTE: wasi doesn't really do full paths afact.
fullpath := clone_string(name, allocator) or_return
return internal_stat(stat, fullpath), nil
}
_lstat :: proc(name: string, allocator: runtime.Allocator) -> (fi: File_Info, err: Error) {
if name == "" {
err = .Invalid_Path
return
}
dir_fd, relative, ok := match_preopen(name)
if !ok {
err = .Invalid_Path
return
}
stat, _err := wasi.path_filestat_get(dir_fd, {}, relative)
if _err != nil {
err = _get_platform_error(_err)
return
}
// NOTE: wasi doesn't really do full paths afact.
fullpath := clone_string(name, allocator) or_return
return internal_stat(stat, fullpath), nil
}
_same_file :: proc(fi1, fi2: File_Info) -> bool {
return fi1.fullpath == fi2.fullpath
}
+9
View File
@@ -0,0 +1,9 @@
#+private
package os2
import "base:runtime"
_temp_dir :: proc(allocator: runtime.Allocator) -> (string, runtime.Allocator_Error) {
// NOTE: requires user to add /tmp to their preopen dirs, no standard way exists.
return clone_string("/tmp", allocator)
}
+1 -1
View File
@@ -1287,7 +1287,7 @@ sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: soc
}
send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Error) {
result := _unix_send(c.int(sd), raw_data(data), len(data), 0)
result := _unix_send(c.int(sd), raw_data(data), len(data), i32(flags))
if result < 0 {
return 0, get_last_error()
}
+1 -1
View File
@@ -1155,7 +1155,7 @@ sendto :: proc(sd: Socket, data: []u8, flags: int, addr: ^SOCKADDR, addrlen: soc
}
send :: proc(sd: Socket, data: []byte, flags: int) -> (u32, Error) {
result := unix.sys_sendto(int(sd), raw_data(data), len(data), 0, nil, 0)
result := unix.sys_sendto(int(sd), raw_data(data), len(data), flags, nil, 0)
if result < 0 {
return 0, _get_errno(int(result))
}