From 8f6d0d205ae064624aae3805d61089190c43b218 Mon Sep 17 00:00:00 2001 From: Tjeu Kayim <15987676+TjeuKayim@users.noreply.github.com> Date: Sun, 6 Nov 2022 15:35:29 +0100 Subject: [PATCH 1/3] lib: add ctypes foreign function interface --- CMakeLists.txt | 14 +- examples/ctypes.uc | 57 ++++++ lib/ctypes.c | 486 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 examples/ctypes.uc create mode 100644 lib/ctypes.c diff --git a/CMakeLists.txt b/CMakeLists.txt index d8dd34ab..8b2c7c6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ include(CheckFunctionExists) include(CheckSymbolExists) PROJECT(ucode C) -ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -ffunction-sections -fwrapv -D_GNU_SOURCE) +ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu11 -ffunction-sections -fwrapv -D_GNU_SOURCE) IF(CMAKE_C_COMPILER_VERSION VERSION_GREATER 6) ADD_DEFINITIONS(-Wextra -Werror=implicit-function-declaration) @@ -27,6 +27,7 @@ OPTION(RTNL_SUPPORT "Route Netlink plugin support" ${LINUX}) OPTION(NL80211_SUPPORT "Wireless Netlink plugin support" ${LINUX}) OPTION(RESOLV_SUPPORT "NS resolve plugin support" ON) OPTION(STRUCT_SUPPORT "Struct plugin support" ON) +OPTION(CTYPES_SUPPORT "Ctypes plugin support" ON) OPTION(ULOOP_SUPPORT "Uloop plugin support" ON) SET(LIB_SEARCH_PATH "${CMAKE_INSTALL_PREFIX}/lib/ucode/*.so:${CMAKE_INSTALL_PREFIX}/share/ucode/*.uc:./*.so:./*.uc" CACHE STRING "Default library search path") @@ -192,6 +193,17 @@ IF(STRUCT_SUPPORT) ENDIF() ENDIF() +IF(CTYPES_SUPPORT) + FIND_LIBRARY(ffi NAMES ffi) + FIND_PATH(ffi_include_dir NAMES ffi.h) + INCLUDE_DIRECTORIES(${ffi_include_dir}) + SET(LIBRARIES ${LIBRARIES} ctypes_lib) + ADD_LIBRARY(ctypes_lib MODULE lib/ctypes.c) + SET_TARGET_PROPERTIES(ctypes_lib PROPERTIES OUTPUT_NAME ctypes PREFIX "") + TARGET_LINK_OPTIONS(ctypes_lib PRIVATE ${UCODE_MODULE_LINK_OPTIONS}) + TARGET_LINK_LIBRARIES(ctypes_lib ${ffi}) +ENDIF() + IF(ULOOP_SUPPORT) FIND_LIBRARY(ubox NAMES ubox) FIND_PATH(uloop_include_dir NAMES libubox/uloop.h) diff --git a/examples/ctypes.uc b/examples/ctypes.uc new file mode 100644 index 00000000..d3cbfe05 --- /dev/null +++ b/examples/ctypes.uc @@ -0,0 +1,57 @@ +const c = require("ctypes"); +const struct = require("struct"); + +const sizeof = (abbreviation) => length(struct.pack(abbreviation)); + +const abbreviation_to_ffi = { + i: c.ffi_type.sint, + P: c.ffi_type.pointer, + N: c.ffi_type["uint" + sizeof("N") * 8], +}; + +function attach(dl_handle, fun) { + const params_list = split(fun.params, ""); + const cif = c.prep( + c.const.FFI_DEFAULT_ABI, + ...map(params_list, (a) => abbreviation_to_ffi[a]) + ); + return function (...args) { + const packed = struct.pack(fun.params, 0, ...args); + const return_buffer = c.ptr(packed); + const s = c.symbol(dl_handle, fun.name); + assert(s != null); + assert(cif.call(s, return_buffer)); + return struct.unpack( + substr(fun.params, 0, 1), + return_buffer.ucv_string_new() + )[0]; + }; +} + +const libc = {}; +for (fun in [ + { name: "dlopen", params: "PPi" }, + { name: "strlen", params: "NP" }, +]) { + libc[fun.name] = attach(c.const.RTLD_DEFAULT, fun); +} + +function dlopen(library_name) { + const library_name_copy = c.ptr(library_name); + const return_ptr = libc.dlopen(library_name_copy.as_int(), c.const.RTLD_NOW); + assert(library_name_copy.drop()); + assert(return_ptr != 0); + return c.ptr(return_ptr); +} + +const c_sqlite_version = attach(dlopen("libsqlite3.so.0"), { + name: "sqlite3_libversion", + params: "P", +}); +function sqlite_version() { + const return_ptr = c_sqlite_version(); + const len = libc.strlen(return_ptr); + return c.ptr(return_ptr).ucv_string_new(len); +} + +print("sqlite version: ", sqlite_version(), "\n"); diff --git a/lib/ctypes.c b/lib/ctypes.c new file mode 100644 index 00000000..899ac7e2 --- /dev/null +++ b/lib/ctypes.c @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2022 Tjeu Kayim <15987676+TjeuKayim@users.noreply.github.com> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "ucode/module.h" + +static uc_resource_type_t *ptr_type; +static uc_resource_type_t *cif_type; + +typedef struct { + void *void_ptr; + void (*free)(void *); + size_t initialized_bytes; +} ptr_box_t; + +typedef struct { + ffi_cif cif; + // reusable buffer to store the argument types and argument value pointers + void *values[0]; +} uc_cif_t; + +static uc_value_t * +ptr_new_common(void *void_ptr, void (*freefn)(void *), size_t initialized) +{ + ptr_box_t *box = xalloc(sizeof(ptr_box_t)); + box->void_ptr = void_ptr; + box->free = freefn; + box->initialized_bytes = initialized; + + // TODO: Consider optimization: this function and uc_struct_new() could store the data directly after the + // uc_value_t, like how ucv_cfunction_new() stores the uc_value_t and the name string in one allocation. + return uc_resource_new(ptr_type, box); +} + +static uc_value_t * +ctypes_symbol(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **handle = (ptr_box_t **)ucv_resource_dataptr(uc_fn_arg(0), "ctypes.ptr"); + uc_value_t *name = uc_fn_arg(1); + if (nargs != 2 || ucv_type(name) != UC_STRING || !handle || !*handle) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, + "Expected ctypes.ptr and string arguments"); + + return NULL; + } + + void *symbol = dlsym((**handle).void_ptr, ucv_string_get(name)); + + return symbol ? ptr_new_common(symbol, NULL, 0) : NULL; +} + +static uc_value_t * +ctypes_new_ptr(uc_vm_t *vm, size_t nargs) +{ + if (nargs != 1) { + return NULL; + } + + uc_value_t *arg0 = uc_fn_arg(0); + switch (ucv_type(arg0)) { + case UC_INTEGER: + void *void_ptr = (void*) (intptr_t) ucv_int64_get(arg0); + return ptr_new_common(void_ptr, NULL, 0); + case UC_STRING: + // TODO: it would be nice if struct.pack had a variant that returned a pointer, then this conversion is less + // necessary. + char *borrow = ucv_string_get(arg0); + size_t length = ucv_string_length(arg0); + char *clone = xalloc(length); + memcpy(clone, borrow, length); + return ptr_new_common(clone, free, length); + default: + return NULL; + } +} + +static uc_value_t * +ctypes_prepare_cif(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *abi_arg = uc_fn_arg(0); + + if (nargs < 2 || ucv_type(abi_arg) != UC_INTEGER) { + return NULL; + } + + size_t cif_argument_count = nargs - 2; + + size_t types_buffer_length = cif_argument_count * 2 + 1; + uc_cif_t *cif = xalloc( + sizeof(uc_cif_t) + + sizeof(void*) * types_buffer_length + + sizeof(size_t) * (cif_argument_count + 1) + ); + + ffi_type **types = (ffi_type**) &cif->values; + + for (size_t i = 0; i < cif_argument_count + 1; i++) + { + uc_value_t *arg = uc_fn_arg(1 + i); + ffi_type **destination = &types[i]; + bool is_return = i == 0; + + switch (ucv_type(arg)) { + case UC_NULL: + if (!is_return) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Only the return type can be void"); + goto fail; + } + *destination = &ffi_type_void; + break; + case UC_RESOURCE: + ptr_box_t **box = (ptr_box_t **)ucv_resource_dataptr(arg, "ctypes.ptr"); + if (!box || !*box) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected resource type"); + goto fail; + } + ffi_type *type = (**box).void_ptr; + if (!type) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "null ctypes.ptr"); + goto fail; + } + *destination = type; + break; + default: + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected type"); + goto fail; + } + } + + ffi_abi abi = ucv_uint64_get(abi_arg); + if (ffi_prep_cif(&cif->cif, abi, cif_argument_count, types[0], &types[1]) != FFI_OK) { + uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "ffi_prep_cif failed"); + goto fail; + } + + ffi_type arg_struct_type; + arg_struct_type.type = FFI_TYPE_STRUCT; + arg_struct_type.size = 0; + arg_struct_type.alignment = 0; + arg_struct_type.elements = cif->cif.rtype->type == FFI_TYPE_VOID ? &types[1] : &types[0]; + types[cif_argument_count + 1] = NULL; + + size_t *arg_struct_offsets = (size_t*) &cif->values[types_buffer_length]; + if (ffi_get_struct_offsets(abi, &arg_struct_type, arg_struct_offsets) != FFI_OK) { + uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "ffi_get_struct_offsets failed"); + goto fail; + } + + return uc_resource_new(cif_type, cif); + +fail: + free(cif); + return NULL; +} + +static void +ptr_gc(void *ud) +{ + ptr_box_t *box = ud; + + if (box->free) { + box->free(box->void_ptr); + } + + free(box); +} + +static uc_value_t * +ptr_as_int(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **box = uc_fn_this("ctypes.ptr"); + + if (!box || !*box) + return NULL; + + return ucv_int64_new((intptr_t) (**box).void_ptr); +} + +static uc_value_t * +ptr_copy_uc_string(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **box = uc_fn_this("ctypes.ptr"); + + if (!box || !*box) + return NULL; + + if (nargs > 1) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected argument count"); + return NULL; + } + + size_t length = 0; + uc_value_t *length_arg = uc_fn_arg(0); + if (nargs == 1) { + if (ucv_type(length_arg) != UC_INTEGER) { + return NULL; + } + length = ucv_int64_get(length_arg); + } + + size_t initialized_bytes = (**box).initialized_bytes; + if (length == 0) { + length = initialized_bytes; + } + + return ucv_string_new_length((char*) (**box).void_ptr, length); +} + +static uc_value_t * +ptr_drop(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **box = uc_fn_this("ctypes.ptr"); + + if (!box || !*box) + return NULL; + + if ((**box).free) { + (**box).free((**box).void_ptr); + (**box).free = NULL; + return ucv_boolean_new(true); + } + + return ucv_boolean_new(false); +} + +static uc_value_t * +ptr_forget(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **box = uc_fn_this("ctypes.ptr"); + + if (!box || !*box) + return NULL; + + if ((**box).free) { + (**box).free = NULL; + return ucv_boolean_new(true); + } + + return ucv_boolean_new(false); +} + +static uc_value_t * +ptr_tostring(uc_vm_t *vm, size_t nargs) +{ + ptr_box_t **box = uc_fn_this("ctypes.ptr"); + + if (!box || !*box) + return NULL; + + uc_stringbuf_t *buf = ucv_stringbuf_new(); + + ucv_stringbuf_append(buf, "ctypes.ptr( "); + if ((**box).free) { + ucv_stringbuf_append(buf, "garbage-collected "); + } + ucv_stringbuf_printf(buf, "*%p )", (**box).void_ptr); + + return ucv_stringbuf_finish(buf); +} + +static uc_value_t * +cif_call(uc_vm_t *vm, size_t nargs) +{ + uc_cif_t **cif = uc_fn_this("ctypes.cif"); + + if (!cif || !*cif) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected this"); + return NULL; + } + + size_t cif_argument_count = (**cif).cif.nargs; + if (nargs == 1 || nargs > 2) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected argument count"); + return NULL; + } + + size_t types_buffer_length = cif_argument_count * 2 + 1; + size_t *arg_struct_offsets = (size_t*) &(**cif).values[types_buffer_length]; + + ptr_box_t **target_fn = (ptr_box_t **)ucv_resource_dataptr(uc_fn_arg(0), "ctypes.ptr"); + if (!target_fn || !*target_fn) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "First argument must be ctypes.ptr to a function"); + return NULL; + } + + void *return_value; + void **argument_values = (void**) &(**cif).values[cif_argument_count + 1]; + bool returns_void = (**cif).cif.rtype->type == FFI_TYPE_VOID; + + uc_value_t *arg_value = uc_fn_arg(1); + switch (ucv_type(arg_value)) { + case UC_NULL: + if (cif_argument_count != 0 || !returns_void) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Only pass null for void(void) signature"); + return NULL; + } + break; + case UC_RESOURCE: + ptr_box_t **box = (ptr_box_t **)ucv_resource_dataptr(arg_value, "ctypes.ptr"); + if (!box || !*box) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected resource type"); + return NULL; + } + void *void_ptr = (**box).void_ptr; + if (!void_ptr) { + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected null ctypes.ptr"); + return NULL; + } + + size_t arg_struct_index = 0; + if (!returns_void) { + return_value = void_ptr + arg_struct_offsets[0]; + arg_struct_index = 1; + } + + for (size_t i = 0; i < cif_argument_count; i++) + { + argument_values[i] = void_ptr + arg_struct_offsets[arg_struct_index++]; + } + + break; + default: + uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unexpected type"); + return NULL; + } + + void *widened_return_value = return_value; + ffi_sarg sarg; + ffi_arg uarg; + + if ((**cif).cif.rtype->size < sizeof(ffi_arg)) { + // these are special cases in libffi "for historical reasons" + switch ((**cif).cif.rtype->type) { + case FFI_TYPE_SINT8: + case FFI_TYPE_SINT16: + case FFI_TYPE_SINT32: + widened_return_value = &sarg; + break; + case FFI_TYPE_UINT8: + case FFI_TYPE_UINT16: + case FFI_TYPE_UINT32: + widened_return_value = &uarg; + break; + default: + break; + } + } + + ffi_call(&(**cif).cif, FFI_FN((**target_fn).void_ptr), widened_return_value, argument_values); + + if (widened_return_value != return_value) { + switch ((**cif).cif.rtype->type) { + case FFI_TYPE_SINT8: + *(int8_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + case FFI_TYPE_SINT16: + *(int16_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + case FFI_TYPE_SINT32: + *(int32_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + case FFI_TYPE_UINT8: + *(uint8_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + case FFI_TYPE_UINT16: + *(uint16_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + case FFI_TYPE_UINT32: + *(uint32_t*)return_value = *(ffi_sarg*)widened_return_value; + break; + default: + break; + } + } + + return ucv_boolean_new(true); +} + +static void +cif_gc(void *ud) +{ + uc_cif_t *cif = ud; + + free(cif); +} + +static void +register_constants(uc_vm_t *vm, uc_value_t *scope) +{ + uc_value_t *const_object = ucv_object_new(vm); +#define ADD_CONST_PTR(x) ucv_object_add(const_object, #x, ptr_new_common(x, NULL, 0)) +#define ADD_CONST_INT(x) ucv_object_add(const_object, #x, ucv_int64_new(x)) + ADD_CONST_PTR(RTLD_DEFAULT); + ADD_CONST_PTR(RTLD_NEXT); + ADD_CONST_INT(RTLD_NOW); + + ADD_CONST_INT(FFI_LAST_ABI); + ADD_CONST_INT(FFI_DEFAULT_ABI); + // TODO: find a way to iterate over the names of the other ffi_abi enum values even though they differ between + // libffi versions and CPU architectures + + ADD_CONST_INT(FFI_SIZEOF_ARG); + static_assert(sizeof(ffi_arg) == FFI_SIZEOF_ARG, "Expected different ffi_arg size"); + + ucv_object_add(scope, "const", const_object); + + uc_value_t *ffi_types_object = ucv_object_new(vm); +#define ADD_FFI_TYPE(x) ucv_object_add(ffi_types_object, #x, ptr_new_common(&ffi_type_ ## x, NULL, sizeof(ffi_type))) + + ADD_FFI_TYPE(uchar); + ADD_FFI_TYPE(schar); + ADD_FFI_TYPE(ushort); + ADD_FFI_TYPE(sshort); + ADD_FFI_TYPE(uint); + ADD_FFI_TYPE(sint); + ADD_FFI_TYPE(ulong); + ADD_FFI_TYPE(slong); + + // The types below are not macros, so can also be accessed as symbol, e.g.: + // const c = require('ctypes'); c.symbol(c.const.RTLD_NEXT, 'ffi_type_uint8') + // For completeness, they are added here anyway. + ADD_FFI_TYPE(void); + ADD_FFI_TYPE(uint8); + ADD_FFI_TYPE(sint8); + ADD_FFI_TYPE(uint16); + ADD_FFI_TYPE(sint16); + ADD_FFI_TYPE(uint32); + ADD_FFI_TYPE(sint32); + ADD_FFI_TYPE(uint64); + ADD_FFI_TYPE(sint64); + ADD_FFI_TYPE(float); + ADD_FFI_TYPE(double); + ADD_FFI_TYPE(pointer); + ADD_FFI_TYPE(longdouble); + + ucv_object_add(scope, "ffi_type", ffi_types_object); +}; + +static const uc_function_list_t ptr_fns[] = { + { "as_int", ptr_as_int }, + { "ucv_string_new", ptr_copy_uc_string }, + { "drop", ptr_drop }, + { "forget", ptr_forget }, + { "tostring", ptr_tostring }, +}; + +static const uc_function_list_t cif_fns[] = { + { "call", cif_call }, +}; + +static const uc_function_list_t global_fns[] = { + { "ptr", ctypes_new_ptr }, + { "symbol", ctypes_symbol }, + { "prep", ctypes_prepare_cif }, +}; + +void uc_module_init(uc_vm_t *vm, uc_value_t *scope) +{ + uc_function_list_register(scope, global_fns); + + ptr_type = uc_type_declare(vm, "ctypes.ptr", ptr_fns, ptr_gc); + cif_type = uc_type_declare(vm, "ctypes.cif", cif_fns, cif_gc); + + register_constants(vm, scope); +} From 06f4f7c498d62bdaf45736e68afb1f6996d6cd29 Mon Sep 17 00:00:00 2001 From: Packet Please Date: Tue, 4 Apr 2023 02:13:50 +0200 Subject: [PATCH 2/3] ctypes: expose errno Signed-off-by: Packet Please --- lib/ctypes.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/ctypes.c b/lib/ctypes.c index 899ac7e2..b4e455c7 100644 --- a/lib/ctypes.c +++ b/lib/ctypes.c @@ -369,6 +369,8 @@ cif_call(uc_vm_t *vm, size_t nargs) ffi_call(&(**cif).cif, FFI_FN((**target_fn).void_ptr), widened_return_value, argument_values); + uc_vm_registry_set(vm, "ctypes.errno", ucv_int64_new(errno)); + if (widened_return_value != return_value) { switch ((**cif).cif.rtype->type) { case FFI_TYPE_SINT8: @@ -397,6 +399,12 @@ cif_call(uc_vm_t *vm, size_t nargs) return ucv_boolean_new(true); } +static uc_value_t * +ctypes_errno(uc_vm_t *vm, size_t nargs) +{ + return uc_vm_registry_get(vm, "ctypes.errno"); +} + static void cif_gc(void *ud) { @@ -473,6 +481,7 @@ static const uc_function_list_t global_fns[] = { { "ptr", ctypes_new_ptr }, { "symbol", ctypes_symbol }, { "prep", ctypes_prepare_cif }, + { "errno", ctypes_errno }, }; void uc_module_init(uc_vm_t *vm, uc_value_t *scope) @@ -482,5 +491,7 @@ void uc_module_init(uc_vm_t *vm, uc_value_t *scope) ptr_type = uc_type_declare(vm, "ctypes.ptr", ptr_fns, ptr_gc); cif_type = uc_type_declare(vm, "ctypes.cif", cif_fns, cif_gc); + uc_vm_registry_set(vm, "ctypes.errno", ucv_int64_new(0)); + register_constants(vm, scope); } From 88c5c75ed3f30ffeabc33d290f91a984ac0358b4 Mon Sep 17 00:00:00 2001 From: Packet Please Date: Tue, 4 Apr 2023 02:13:23 +0200 Subject: [PATCH 3/3] ctypes: fix maybe-uninitialized warning Signed-off-by: Packet Please --- lib/ctypes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ctypes.c b/lib/ctypes.c index b4e455c7..d4b692fa 100644 --- a/lib/ctypes.c +++ b/lib/ctypes.c @@ -304,7 +304,7 @@ cif_call(uc_vm_t *vm, size_t nargs) return NULL; } - void *return_value; + void *return_value = 0; void **argument_values = (void**) &(**cif).values[cif_argument_count + 1]; bool returns_void = (**cif).cif.rtype->type == FFI_TYPE_VOID;