From 6c39c57cc1388f48fe07e3d68c3d0ac424e5c379 Mon Sep 17 00:00:00 2001 From: Jason Turner Date: Mon, 15 Feb 2021 16:56:40 -0700 Subject: [PATCH] Add example of how we can ask C++ nicely to give us a Python object in exchange for a Ruby object --- CMakeLists.txt | 7 ++ Person.hpp | 20 +++++ ...test_pycall_use_cpp_to_convert_pointers.rb | 78 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 ruby/test_pycall_use_cpp_to_convert_pointers.rb diff --git a/CMakeLists.txt b/CMakeLists.txt index 374ab25..f037848 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -232,4 +232,11 @@ if(BUILD_RUBY_BINDINGS AND BUILD_PYTHON_BINDINGS) COMMAND ${Ruby_EXECUTABLE} test_pycall_workaround.rb ) + add_test( + NAME test_pycall_use_cpp_to_convert_pointers + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/ruby + COMMAND ${Ruby_EXECUTABLE} test_pycall_use_cpp_to_convert_pointers.rb + ) + + endif() diff --git a/Person.hpp b/Person.hpp index 4c8bbfd..156feb8 100644 --- a/Person.hpp +++ b/Person.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace Test { @@ -23,6 +24,25 @@ std::ostream& operator<<(std::ostream&, const Test::Person&); // A free-standing function taking an object as argument std::string personName(const Person& person); +inline void setName(Person &p, const std::string &newname) { + p.setName(newname); +} + +// get an integral representation of the pointer that is this Person +inline long long toInt(Person &p) { + std::clog << "original pointer: " << &p << '\n'; + const auto result = reinterpret_cast(&p); + std::clog << "toInt from C++ " << result << '\n'; + return result; +} + +// take the integer from toInt and reinterpret_cast it back into a Person *, then return that as a reference +inline Person &fromInt(long long i) { + auto *ptr = reinterpret_cast(i); + std::clog << "Reclaimed pointer: " << ptr << '\n'; + return *ptr; +} + } // Namespace Test diff --git a/ruby/test_pycall_use_cpp_to_convert_pointers.rb b/ruby/test_pycall_use_cpp_to_convert_pointers.rb new file mode 100644 index 0000000..91be872 --- /dev/null +++ b/ruby/test_pycall_use_cpp_to_convert_pointers.rb @@ -0,0 +1,78 @@ +ruby_lib_path = File.expand_path(File.join(__dir__, '../build/Products/ruby/mylib.so')) + +if !File.exist?(ruby_lib_path) + puts "Error, this assumes you already built the project in /build" + exit 1 +end + +require ruby_lib_path + + +puts "================== RUBY SIDE ======================" +ruby_p = Mylib::Person.new("John") +puts "ruby_p=#{ruby_p}" +puts "Mylib::personName(ruby_p)= #{Mylib::personName(ruby_p)}" + + +puts "\n\n" + +puts "================== PYTHON SIDE ======================" + +require 'pycall/import' +include PyCall::Import + + +python_lib_path = File.expand_path(File.join(__dir__, '../build/Products/python/mylib.py')) +if !File.exist?(python_lib_path) + puts "Error, this assumes you already built the Python project in /build" + exit 1 +end + +sys = PyCall.import_module('sys') +sys.path.insert(0, "#{File.dirname(python_lib_path)}") + +pyimport 'mylib', as: 'mylib_python' +python_p = mylib_python.Person.new("John") +puts "python_p=#{python_p}" +puts "mylib_python.personName(python_p)= #{mylib_python.personName(python_p)}" + + +puts "\n\n" + +puts "================== PASSING RUBY TO PYTHON ======================" + +# We're going to use C++ to marshal between a ruby object and a python object +# We take the already created ruby_p object and get back an int. +# +# What is the int (actually unsigned long long)? it's just the underlying pointer. +# Why an int? Because we need a type that Python, Ruby, and PyCall all recognize +# +# Next we take that int and say "please give me a Python Person object from this" +# +# What does fromInt do? It simply reinterpret_casts the integral value back into a Person & +# +# Note: it is critical that we call toInt from Ruby (because it's a Ruby `Person`) +# and critical that we call fromInt from Python (because we want a Python `Person`) +# +# **** Very Important: there are *all kinds of ways* this can go wrong. we need to provide a +# handy wrapper in ruby that gets a python object back and not directly tell the users +# of either library about these fromInt/toInt functions + +python_p2 = mylib_python.fromInt(Mylib::toInt(ruby_p)) + +# Now ruby_p is still the same object +ruby_name = ruby_p.getName() +puts "ruby_name=#{ruby_name}" + +# And python_p2 is a reference to the ruby_p object. +mylib_python.personName(python_p2) + +# We verify this reference relationship asking Python to update the name +mylib_python.setName(python_p2, "Bob") + +# Then verifying that it's the name we now expect +puts "Ruby name snould now be 'Bob' also: #{ruby_p.getName()}" + + + +