diff --git a/source/loaders/rb_loader/include/rb_loader/rb_loader_include.h b/source/loaders/rb_loader/include/rb_loader/rb_loader_include.h index 8d2c0a88ee..22d23eea27 100644 --- a/source/loaders/rb_loader/include/rb_loader/rb_loader_include.h +++ b/source/loaders/rb_loader/include/rb_loader/rb_loader_include.h @@ -37,6 +37,7 @@ #endif #include +#include /* Disable warnings from Ruby */ #if defined(__clang__) diff --git a/source/loaders/rb_loader/source/rb_loader_impl.c b/source/loaders/rb_loader/source/rb_loader_impl.c index 439304bb78..d748286b9f 100644 --- a/source/loaders/rb_loader/source/rb_loader_impl.c +++ b/source/loaders/rb_loader/source/rb_loader_impl.c @@ -10,6 +10,10 @@ #include #include +#if RUBY_API_VERSION_MAJOR >= 3 && RUBY_API_VERSION_MINOR >= 1 + #include +#endif + #include #include @@ -180,13 +184,17 @@ const char *rb_type_deserialize(loader_impl impl, VALUE v, value *result) else if (v_type == T_STRING) { long length = RSTRING_LEN(v); - char *str = StringValuePtr(v); if (length > 0 && str != NULL) { *result = value_create_string(str, (size_t)length); } + else + { + /* TODO: Return exception in the future? */ + *result = value_create_string("", 0); + } return "String"; } @@ -216,6 +224,38 @@ const char *rb_type_deserialize(loader_impl impl, VALUE v, value *result) return "NilClass"; } + else if (v_type == T_HASH) + { + size_t size = (size_t)rb_hash_size_num(v); + *result = value_create_map(NULL, size); + if (size > 0 && *result != NULL) + { + value *v_map_ptr = value_to_map(*result); + size_t i = 0; + VALUE keys = rb_funcall(v, rb_intern("keys"), 0); + for (i = 0; i < size; ++i) + { + VALUE key = rb_ary_entry(keys, (long)i); + VALUE val = rb_hash_aref(v, key); + value *pair = value_create_array(NULL, 2); + value *pair_ptr = value_to_array(pair); + (void)rb_type_deserialize(impl, key, &pair_ptr[0]); + (void)rb_type_deserialize(impl, val, &pair_ptr[1]); + v_map_ptr[i] = pair; + } + } + return "Hash"; + } +#if RUBY_API_VERSION_MAJOR >= 3 && RUBY_API_VERSION_MINOR >= 1 + else if (rb_obj_is_kind_of(v, rb_cIOBuffer)) + { + const void *base; + size_t size; + rb_io_buffer_get_bytes_for_reading(v, &base, &size); + *result = value_create_buffer(base, size); + return "IO::Buffer"; + } +#endif else if (v_type == T_OBJECT) { loader_impl_rb_object rb_obj = malloc(sizeof(struct loader_impl_rb_object_type)); @@ -289,6 +329,14 @@ VALUE rb_type_serialize(value v) { return (value_to_bool(v) == 0L) ? Qfalse : Qtrue; } + else if (v_type == TYPE_CHAR) + { + return INT2NUM((int)value_to_char(v)); + } + else if (v_type == TYPE_SHORT) + { + return INT2NUM((int)value_to_short(v)); + } else if (v_type == TYPE_INT) { return INT2NUM(value_to_int(v)); @@ -315,6 +363,46 @@ VALUE rb_type_serialize(value v) { return Qnil; } + else if (v_type == TYPE_BUFFER) + { + const char *buf = value_to_buffer(v); + size_t size = value_type_size(v); + /* Copy the data into a Ruby String first to avoid memory lifetime issues */ + VALUE str = rb_str_new(buf, (long)size); +#if RUBY_API_VERSION_MAJOR >= 3 && RUBY_API_VERSION_MINOR >= 1 + /* TODO: Reference original memory instead of copying once lifetime is guaranteed */ + /* return rb_io_buffer_new((void *)buf, size, RB_IO_BUFFER_READONLY); */ + return rb_io_buffer_new(RSTRING_PTR(str), size, RB_IO_BUFFER_READONLY); +#else + /* Fallback when ruby/io/buffer.h is unavailable: return as binary string */ + return str; +#endif + } + else if (v_type == TYPE_ARRAY) + { + size_t size = value_type_count(v); + VALUE arr = rb_ary_new2((long)size); + value *v_array = value_to_array(v); + for (size_t i = 0; i < size; ++i) + { + rb_ary_store(arr, (long)i, rb_type_serialize(v_array[i])); + } + return arr; + } + else if (v_type == TYPE_MAP) + { + size_t size = value_type_count(v); + VALUE hash = rb_hash_new(); + value *v_map = value_to_map(v); + for (size_t i = 0; i < size; ++i) + { + value *pair = value_to_array(v_map[i]); + VALUE key = rb_type_serialize(pair[0]); + VALUE val = rb_type_serialize(pair[1]); + rb_hash_aset(hash, key, val); + } + return hash; + } else { rb_raise(rb_eArgError, "Unsupported return type"); diff --git a/source/tests/CMakeLists.txt b/source/tests/CMakeLists.txt index 55ddb2b41a..e9b053a139 100644 --- a/source/tests/CMakeLists.txt +++ b/source/tests/CMakeLists.txt @@ -144,6 +144,7 @@ add_subdirectory(metacall_node_extension_test) add_subdirectory(metacall_node_napi_test) add_subdirectory(metacall_node_multithread_deadlock_test) add_subdirectory(metacall_node_conversion_test) +add_subdirectory(metacall_ruby_conversion_test) add_subdirectory(metacall_distributable_test) add_subdirectory(metacall_cast_test) add_subdirectory(metacall_init_fini_test) diff --git a/source/tests/metacall_ruby_conversion_test/CMakeLists.txt b/source/tests/metacall_ruby_conversion_test/CMakeLists.txt new file mode 100644 index 0000000000..e2172efb1a --- /dev/null +++ b/source/tests/metacall_ruby_conversion_test/CMakeLists.txt @@ -0,0 +1,156 @@ +# Check if this loader is enabled +if(NOT OPTION_BUILD_LOADERS OR NOT OPTION_BUILD_LOADERS_RB) + return() +endif() + +# +# Executable name and options +# + +# Target name +set(target metacall-ruby-conversion-test) +message(STATUS "Test ${target}") + +# +# Compiler warnings +# + +include(Warnings) + +# +# Compiler security +# + +include(SecurityFlags) + +# +# Sources +# + +set(include_path "${CMAKE_CURRENT_SOURCE_DIR}/include/${target}") +set(source_path "${CMAKE_CURRENT_SOURCE_DIR}/source") + +set(sources + ${source_path}/main.cpp + ${source_path}/metacall_ruby_conversion_test.cpp +) + +# Group source files +set(header_group "Header Files (API)") +set(source_group "Source Files") +source_group_by_path(${include_path} "\\\\.h$|\\\\.hpp$" + ${header_group} ${headers}) +source_group_by_path(${source_path} "\\\\.cpp$|\\\\.c$|\\\\.h$|\\\\.hpp$" + ${source_group} ${sources}) + +# +# Create executable +# + +# Build executable +add_executable(${target} + ${sources} +) + +# Create namespaced alias +add_executable(${META_PROJECT_NAME}::${target} ALIAS ${target}) + +# +# Project options +# + +set_target_properties(${target} + PROPERTIES + ${DEFAULT_PROJECT_OPTIONS} + FOLDER "${IDE_FOLDER}" +) + +# +# Include directories +# + +target_include_directories(${target} + PRIVATE + ${DEFAULT_INCLUDE_DIRECTORIES} + ${PROJECT_BINARY_DIR}/source/include +) + +# +# Libraries +# + +target_link_libraries(${target} + PRIVATE + ${DEFAULT_LIBRARIES} + + GTest + + ${META_PROJECT_NAME}::metacall +) + +# +# Compile definitions +# + +target_compile_definitions(${target} + PRIVATE + ${DEFAULT_COMPILE_DEFINITIONS} +) + +# +# Compile options +# + +target_compile_options(${target} + PRIVATE + ${DEFAULT_COMPILE_OPTIONS} +) + +# +# Compile features +# + +target_compile_features(${target} + PRIVATE + cxx_std_17 +) + +# +# Linker options +# + +target_link_options(${target} + PRIVATE + ${DEFAULT_LINKER_OPTIONS} +) + +# +# Define test +# + +add_test(NAME ${target} + COMMAND $ +) + +# +# Define dependencies +# + +add_dependencies(${target} + rb_loader +) + +# +# Define test properties +# + +set_property(TEST ${target} + PROPERTY LABELS ${target} +) + +include(TestEnvironmentVariables) + +test_environment_variables(${target} + "" + ${TESTS_ENVIRONMENT_VARIABLES} +) diff --git a/source/tests/metacall_ruby_conversion_test/source/main.cpp b/source/tests/metacall_ruby_conversion_test/source/main.cpp new file mode 100644 index 0000000000..fb41f44af8 --- /dev/null +++ b/source/tests/metacall_ruby_conversion_test/source/main.cpp @@ -0,0 +1,28 @@ +/* + * MetaCall Library by Parra Studios + * A library for providing a foreign function interface calls. + * + * Copyright (C) 2016 - 2026 Vicente Eduardo Ferrer Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} diff --git a/source/tests/metacall_ruby_conversion_test/source/metacall_ruby_conversion_test.cpp b/source/tests/metacall_ruby_conversion_test/source/metacall_ruby_conversion_test.cpp new file mode 100644 index 0000000000..20106eca71 --- /dev/null +++ b/source/tests/metacall_ruby_conversion_test/source/metacall_ruby_conversion_test.cpp @@ -0,0 +1,178 @@ +/* + * MetaCall Library by Parra Studios + * A library for providing a foreign function interface calls. + * + * Copyright (C) 2016 - 2026 Vicente Eduardo Ferrer Garcia + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include +#include +#include + +class metacall_ruby_conversion_test : public testing::Test +{ +public: +}; + +TEST_F(metacall_ruby_conversion_test, DefaultConstructor) +{ + metacall_print_info(); + + metacall_log_null(); + + ASSERT_EQ((int)0, (int)metacall_initialize()); + + { + static const char script[] = "def identity(x); x; end"; + void *handle = NULL; + + ASSERT_EQ((int)0, (int)metacall_load_from_memory("rb", script, sizeof(script), &handle)); + + { + void *args[1] = { metacall_value_create(METACALL_INT) }; + + void *ret = metacallhv(handle, "identity", args); + + EXPECT_NE((void *)NULL, (void *)ret); + + EXPECT_EQ((enum metacall_value_id)METACALL_INT, (enum metacall_value_id)metacall_value_id(ret)); + + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test BOOL round-trip */ + { + void *args[1] = { metacall_value_create_bool(1L) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_BOOL, (enum metacall_value_id)metacall_value_id(ret)); + EXPECT_EQ((boolean)1L, metacall_value_to_bool(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test INT round-trip */ + { + void *args[1] = { metacall_value_create_int(42) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_INT, (enum metacall_value_id)metacall_value_id(ret)); + EXPECT_EQ((int)42, metacall_value_to_int(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test LONG round-trip */ + { + void *args[1] = { metacall_value_create_long(123456789L) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_INT, (enum metacall_value_id)metacall_value_id(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test DOUBLE round-trip */ + { + void *args[1] = { metacall_value_create_double(3.14) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_DOUBLE, (enum metacall_value_id)metacall_value_id(ret)); + EXPECT_DOUBLE_EQ((double)3.14, metacall_value_to_double(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test STRING round-trip */ + { + void *args[1] = { metacall_value_create_string("hello", 5) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_STRING, (enum metacall_value_id)metacall_value_id(ret)); + EXPECT_STREQ("hello", metacall_value_to_string(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test ARRAY round-trip */ + { + void *arr[3] = { + metacall_value_create_int(1), + metacall_value_create_int(2), + metacall_value_create_int(3) + }; + void *args[1] = { metacall_value_create_array((const void **)arr, 3) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_ARRAY, (enum metacall_value_id)metacall_value_id(ret)); + void **ret_arr = metacall_value_to_array(ret); + EXPECT_EQ((size_t)3, metacall_value_count(ret)); + EXPECT_EQ((int)1, metacall_value_to_int(ret_arr[0])); + EXPECT_EQ((int)2, metacall_value_to_int(ret_arr[1])); + EXPECT_EQ((int)3, metacall_value_to_int(ret_arr[2])); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test MAP round-trip */ + { + void *pairs[2]; + void *kv0[2] = { metacall_value_create_string("key", 3), metacall_value_create_int(99) }; + void *kv1[2] = { metacall_value_create_string("lang", 4), metacall_value_create_string("ruby", 4) }; + pairs[0] = metacall_value_create_array((const void **)kv0, 2); + pairs[1] = metacall_value_create_array((const void **)kv1, 2); + void *args[1] = { metacall_value_create_map((const void **)pairs, 2) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_MAP, (enum metacall_value_id)metacall_value_id(ret)); + EXPECT_EQ((size_t)2, metacall_value_count(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + /* Test BUFFER round-trip */ + { + const char data[] = { 0x01, 0x02, 0x03, 0x04 }; + void *args[1] = { metacall_value_create_buffer(data, sizeof(data)) }; + void *ret = metacallhv(handle, "identity", args); + EXPECT_NE((void *)NULL, (void *)ret); + EXPECT_EQ((enum metacall_value_id)METACALL_BUFFER, (enum metacall_value_id)metacall_value_id(ret)); + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + + // TODO: We need to implement assertions + // TODO: for (size_t id = 0; id < METACALL_SIZE; ++id) + for (size_t id = 0; id <= METACALL_MAP; ++id) + { + void *args[1] = { metacall_value_create((enum metacall_value_id)id) }; + + void *ret = metacallhv(handle, "identity", args); + + EXPECT_NE((void *)NULL, (void *)ret); + + std::cout << metacall_value_id_name((enum metacall_value_id)id) << " => " << metacall_value_id_name(metacall_value_id(ret)) << std::endl; + + metacall_value_destroy(ret); + metacall_value_destroy(args[0]); + } + } + + metacall_destroy(); +}