diff --git a/.github/workflows/ruby_tests.yml b/.github/workflows/ruby_tests.yml new file mode 100644 index 00000000..082a315d --- /dev/null +++ b/.github/workflows/ruby_tests.yml @@ -0,0 +1,30 @@ +name: Ruby wrapper building and tests run + +on: + push: + branches: ['main', 'dev'] + +jobs: + test_api: + runs-on: ubuntu-latest + steps: + - name: 'Checkout repo' + uses: actions/checkout@v4 + - name: 'Set up Ruby 3.3' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + - name: 'Install CMake' + run: | + sudo apt-get update -y + sudo apt-get install -y cmake + - name: 'Build native gem' + run: | + gem build vmaware-rb.gemspec + gem install vmaware-rb-1.0.0.gem + - name: Install test deps + run: | + gem install minitest -v 6.0.0 + - name: Run ruby wrapper tests + run: | + ruby gem/test/unit/api.rb diff --git a/.gitignore b/.gitignore index de02920d..8e5d1595 100755 --- a/.gitignore +++ b/.gitignore @@ -1,72 +1,78 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.li - -# Executables -*.out -*.app -main - -#Custom -vm -cli -vmaware -tmp -test -tmp.cpp -src/vmtest.cpp -archive/ -.vscode/ -build/ -milestones.md -bin/ -notes.md -private/ -resources/ -releases/ -release_notes.txt -*.bk -cmake-build-*/ -.idea/* -*.bkp -*copy.hpp -personal_todo.md -notes.txt -auxiliary/test_template.cpp -release_notes.md -src/gui/lib/* -pafish/ -list.txt -/.vs -/.vs/CMake Overview -/.vs/ProjectSettings.json -/.vs/slnx.sqlite -/.vs/VSWorkspaceState.json -TODO.md -auxiliary/path* -packages/ -auxiliary/test.cpp \ No newline at end of file +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.li + +# Executables +*.out +*.app +main + +# Ruby artifacts +*.gem + +#Custom +vm +cli +vmaware +tmp +test +tmp.cpp +src/vmtest.cpp +archive/ +.vscode/ +build/ +milestones.md +bin/ +notes.md +private/ +resources/ +releases/ +release_notes.txt +*.bk +cmake-build-*/ +.idea/* +*.bkp +*copy.hpp +personal_todo.md +notes.txt +auxiliary/test_template.cpp +release_notes.md +src/gui/lib/* +pafish/ +list.txt +/.vs +/.vs/CMake Overview +/.vs/ProjectSettings.json +/.vs/slnx.sqlite +/.vs/VSWorkspaceState.json +TODO.md +auxiliary/path* +packages/ +auxiliary/test.cpp +.devcontainer/ + +!gem/test/ \ No newline at end of file diff --git a/gem/Readme.md b/gem/Readme.md new file mode 100644 index 00000000..612a0c87 --- /dev/null +++ b/gem/Readme.md @@ -0,0 +1,10 @@ +# VMAware-rb + +_A `ruby` wrapper for VMAware._ + +## Notes +- The gem is not supported on windows. +- Builds a native gem. +- Only exports two functions: `vm?` (`VM::detect`) and `confidence`(`VM::percentage`) in their default invocation. + +> If building under `gem install vmaware-rb` starts complaining about a missing `make install` step, update your rubygems (`gem update --system`). \ No newline at end of file diff --git a/gem/extension/CMakeLists.txt b/gem/extension/CMakeLists.txt new file mode 100644 index 00000000..e171de9f --- /dev/null +++ b/gem/extension/CMakeLists.txt @@ -0,0 +1,221 @@ +# ----------------------------------------------------------------------------- +# CMake Version and C++ Standard +# ----------------------------------------------------------------------------- +# CMake 3.26+ is required for modern FetchContent features and improved +# Ruby detection. Rice requires C++17 for features like std::string_view, +# structured bindings, and if-constexpr. + +cmake_minimum_required(VERSION 3.26 FATAL_ERROR) + +if (LINUX) + find_program(CLANGPP_EXECUTABLE NAMES clang++) + find_program(GPP_EXECUTABLE NAMES g++) + + # Preference for clang++ + if(CLANGPP_EXECUTABLE) + set(CMAKE_CXX_COMPILER "${CLANGPP_EXECUTABLE}") + get_filename_component(COMPILER_NAME ${CLANGPP_EXECUTABLE} NAME) + elseif(GPP_EXECUTABLE) + set(CMAKE_CXX_COMPILER "${GPP_EXECUTABLE}") + get_filename_component(COMPILER_NAME ${GPP_EXECUTABLE} NAME) + endif() +endif() + +# ----------------------------------------------------------------------------- +# Project Definition +# ----------------------------------------------------------------------------- +# Define the project name and specify that we only need a C++ compiler. +# The project name is used throughout via ${CMAKE_PROJECT_NAME}. + +project(vmaware LANGUAGES CXX) + +# set C++ standard +if(NOT DEFINED CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 20) +endif() +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_compile_definitions(__VMAWARE_RELEASE__) + +set(PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../") +set(BUILD_DIR "${PROJECT_DIR}/build") + +set(CMAKE_EXPORT_COMPILE_COMMANDS "ON") +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION "ON") + + +# compiler flags +set(CMAKE_CXX_FLAGS "-Wextra -Wall -Wconversion -Wdouble-promotion -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion -ftemplate-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden") + +if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -lstdc++ -lm") +endif() + +if(APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g0 -O2 -Wno-unused-private-field -DNDEBUG") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,-dead_strip -Wl,-x") +elseif(LINUX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g0 -O2 -DNDEBUG") + set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-Wl,--exclude-libs,ALL -Wl,--strip-all") + option(VMAWARE_NATIVE_ARCH "Optimize for the build machine's CPU (non-portable binary)" OFF) + if(VMAWARE_NATIVE_ARCH) + if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native -mtune=native") + elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm|aarch64") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=native") + endif() + endif() +else() + message(FATAL_ERROR "Unsupported Os & compiler combination.") +endif() + +# ----------------------------------------------------------------------------- +# Create the Extension Library +# ----------------------------------------------------------------------------- +# IMPORTANT: Use MODULE instead of SHARED for Ruby extensions! +# +# - MODULE: Creates a loadable plugin that cannot be linked against. +# This is correct for Ruby extensions loaded via require/dlopen. +# - SHARED: Creates a shared library that can be linked against. +# On macOS, this creates a .dylib which Ruby cannot load. + +add_library(${CMAKE_PROJECT_NAME} MODULE) + +# ----------------------------------------------------------------------------- +# Fetch Rice from GitHub +# ----------------------------------------------------------------------------- +# Rice is a header-only library, so we use FetchContent to download it +# automatically. This eliminates the need for users to manually install Rice. +# +# FetchContent downloads the repository at configure time and makes it +# available as if it were part of your project. +# +# Note: For production gems, you may want to pin to a specific release tag +# instead of 'dev' for reproducible builds. + +include(FetchContent) +FetchContent_Declare( + rice + GIT_REPOSITORY https://github.com/ruby-rice/rice.git + GIT_TAG 4.11.4 +) +FetchContent_MakeAvailable(rice) + +# ----------------------------------------------------------------------------- +# Configure Ruby Detection +# ----------------------------------------------------------------------------- +# Rice provides an enhanced FindRuby.cmake that creates proper CMake targets +# (Ruby::Ruby, Ruby::Module) instead of just setting variables. We prepend +# Rice's module path so CMake finds this improved version. +# +# The upstream CMake FindRuby.cmake is being updated to support these targets, +# but until that lands, we use Rice's version. + +list(PREPEND CMAKE_MODULE_PATH "${rice_SOURCE_DIR}") + +# ----------------------------------------------------------------------------- +# Find Ruby Installation +# ----------------------------------------------------------------------------- +# find_package(Ruby) locates the Ruby installation and sets up: +# - Ruby::Ruby - Target for embedding Ruby (links to libruby) +# - Ruby::Module - Target for extensions (links to Ruby headers only) +# +# For extensions, always link to Ruby::Module, not Ruby::Ruby! +# Extensions are loaded into an already-running Ruby process, so they +# should not link against libruby (which could cause symbol conflicts). + +find_package(Ruby REQUIRED) + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE + Ruby::Module +) + +# ----------------------------------------------------------------------------- +# Link to Rice +# ----------------------------------------------------------------------------- +# The Rice::Rice target provides: +# - Include paths to Rice headers +# - Required compiler flags for Rice +# +# Rice is header-only, so this doesn't add any link-time dependencies. + +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE + Rice::Rice +) + +# ----------------------------------------------------------------------------- +# Include Directories for Project Headers +# ----------------------------------------------------------------------------- +# Add the current directory to the include path so we can find +# our project's header files + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE .) +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE "${rice_SOURCE_DIR}/include/") +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ../../src) + +# ----------------------------------------------------------------------------- +# Configure Extension Output +# ----------------------------------------------------------------------------- +# Ruby extensions have specific naming requirements that vary by platform: +# +# Extension Suffix (from Ruby configuration): +# - Linux: .so +# - macOS: .bundle +# - Windows: .so +# +# Note: Windows uses .so (not .dll) for Ruby extensions by convention. +# +# PREFIX "": Ruby extensions have no 'lib' prefix (unlike regular shared libs) +# +# Visibility Settings: +# - CXX_VISIBILITY_PRESET hidden: Hide all symbols by default +# - VISIBILITY_INLINES_HIDDEN ON: Hide inline function symbols +# - WINDOWS_EXPORT_ALL_SYMBOLS OFF: Don't auto-export on Windows +# +# These settings ensure only the Init_* function is exported, reducing +# binary size and avoiding symbol conflicts with other extensions. +# +# Output Directories: +# - RUNTIME_OUTPUT_DIRECTORY: Where Windows puts .dll/.so files +# - LIBRARY_OUTPUT_DIRECTORY: Where Unix puts .so/.bundle files +# +# On Windows, we place the extension in a Ruby version-specific subdirectory +# (e.g., lib/3.3/) to support multiple Ruby versions simultaneously. + +get_target_property(RUBY_EXT_SUFFIX Ruby::Ruby INTERFACE_RUBY_EXTENSION_SUFFIX) +set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES + PREFIX "" + SUFFIX "${RUBY_EXT_SUFFIX}" + OUTPUT_NAME "vmaware_rb" + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + WINDOWS_EXPORT_ALL_SYMBOLS OFF + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../lib" +) + +# ----------------------------------------------------------------------------- +# Source Files +# ----------------------------------------------------------------------------- +# List all source files for the extension. The naming convention used here is: +# - ProjectName-rb.hpp: Header declaring the Init function +# - ProjectName-rb.cpp: Implementation with Rice bindings +# +# The Init_ function in the .cpp file is the entry point Ruby calls +# when the extension is loaded via 'require'. + +target_sources(${CMAKE_PROJECT_NAME} PRIVATE + "${CMAKE_PROJECT_NAME}-rb.hpp" + "${CMAKE_PROJECT_NAME}-rb.cpp" + "../../src/vmaware.hpp" +) + +# ----------------------------------------------------------------------------- +# Install +# ----------------------------------------------------------------------------- +# RubyGems invokes `make install` after `make`, expecting the extension to be +# copied into CMAKE_INSTALL_PREFIX (the gems extensions directory). +# DESTINATION "." installs directly into that prefix with no subdirectory. + +install(TARGETS ${CMAKE_PROJECT_NAME} + LIBRARY DESTINATION . +) diff --git a/gem/extension/vmaware-rb.cpp b/gem/extension/vmaware-rb.cpp new file mode 100644 index 00000000..efbcc8bf --- /dev/null +++ b/gem/extension/vmaware-rb.cpp @@ -0,0 +1,27 @@ +#include "vmaware-rb.hpp" +#include "vmaware.hpp" + +/** + * Two little wrappers so that the templated function is + * compiled with this hardcoded option, and therefore + * i dont need to use complex function overloading + * when defining the ruby methods. + **/ +bool wrap_detect() { + return VM::detect(VM::DEFAULT); +} + +u_int8_t wrap_percentage() { + return VM::percentage(VM::DEFAULT); +} + + + +void Init_vmaware_rb() { + Rice::Module rb_mVMAware = Rice::define_module("VMAware"); + + Rice::Data_Type rb_cVM = Rice::define_class_under(rb_mVMAware, "VM"); + + rb_cVM.define_singleton_function("vm?", &wrap_detect); + rb_cVM.define_singleton_function("confidence", &wrap_percentage); +} \ No newline at end of file diff --git a/gem/extension/vmaware-rb.hpp b/gem/extension/vmaware-rb.hpp new file mode 100644 index 00000000..bc17171e --- /dev/null +++ b/gem/extension/vmaware-rb.hpp @@ -0,0 +1,5 @@ +#include + +extern "C" +__attribute__((visibility("default"))) +void Init_vmaware_rb(); \ No newline at end of file diff --git a/gem/lib/vmaware-rb.rb b/gem/lib/vmaware-rb.rb new file mode 100644 index 00000000..5386048a --- /dev/null +++ b/gem/lib/vmaware-rb.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative 'vmaware_rb' \ No newline at end of file diff --git a/gem/test/unit/api.rb b/gem/test/unit/api.rb new file mode 100644 index 00000000..ccb022d6 --- /dev/null +++ b/gem/test/unit/api.rb @@ -0,0 +1,29 @@ +require 'minitest/autorun' +require 'vmaware-rb' + +raise "vmaware-rb gem failed to load VMAware module" unless defined?(VMAware) +raise "vmaware-rb gem failed to load VMAware::VM class" unless defined?(VMAware::VM) + +class ApiTest < Minitest::Test + + def test_responds_to_check + assert_respond_to VMAware::VM, :vm? + end + + def test_responds_to_confidence + assert_respond_to VMAware::VM, :confidence + end + + def test_vm_check_returns_boolean + result = VMAware::VM.vm? + assert_includes [true, false], result, "vm? must return true or false, got #{result.inspect}" + end + + def test_confidence_returns_integer_in_range + result = VMAware::VM.confidence + assert_kind_of Integer, result, "confidence must return an Integer, got #{result.class}" + assert_operator result, :>=, 0, "confidence must be >= 0" + assert_operator result, :<=, 100, "confidence must be <= 100" + end + +end diff --git a/vmaware-rb.gemspec b/vmaware-rb.gemspec new file mode 100644 index 00000000..7164e32d --- /dev/null +++ b/vmaware-rb.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +if Gem.win_platform? + raise Gem::Exception, + 'Sadly, the vmaware-rb gem is not available on windows, due to heavy reliance on MSVC.' +end + +Gem::Specification.new do |spec| + spec.name = 'vmaware-rb' + spec.version = '1.0.0' + spec.summary = "A ruby wrapper around the VMAware C++ library's default functionality. " + spec.authors = ['Adam Ruman'] + + spec.license = 'MIT' + spec.homepage = 'https://github.com/kernelwernel/VMAware' + + spec.extensions = ['gem/extension/CMakeLists.txt'] + spec.require_paths = ['gem/lib'] + + spec.files = Dir.chdir(__dir__) { Dir[ + 'LICENSE', + 'gem/extension/CMakeLists.txt', + 'gem/extension/vmaware-rb.hpp', + 'gem/extension/vmaware-rb.cpp', + 'gem/lib/vmaware-rb.rb', + 'src/vmaware.hpp' + ] } + + spec.required_ruby_version = '>= 3.3' + spec.metadata['rubygems_mfa_required'] = 'true' + +end