diff --git a/CHANGELOG.md b/CHANGELOG.md index 738b901e9..eb1688679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ and this project aspires to adhere to [Semantic Versioning](https://semver.org/s ### Added #### General +- Added the `cpp_fort_and_py` standalone example. It demos passing Conduit Nodes between C++, Fortran, and Python. See the related tutorial docs (https://llnl-conduit.readthedocs.io/en/latest/tutorial_cpp_fort_and_py.html) for more details. - Added `conduit::utils::info_handler()`, `conduit::utils::warning_handler()`, and `conduit::utils::error_handler()` methods, which provide access to the currently registered info, warning, and error handlers. - Added DataType::index_t method. Creates a DataType instance that describes an `index_t`, which is an alias to either `int32`, or `int 64` controlled by the `CONDUIT_INDEX_32` compile time option. - Added several more methods to Python DataType interface + + #### Relay - Added Relay HDF5 support for reading and writing to an HDF5 dataset with offset. - Added `conduit::relay::io::hdf5::read_info` which allows you to obtain metadata from an HDF5 file. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b4cc2616e..1217a458c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -547,6 +547,38 @@ stages: ./conduit_example displayName: 'Test vs Install (using-with-make)' + - script: | + ################################ + # cpp fort and py example + ################################ + # only run this when python is enabled + # (there might be a better way to do this with an azure `condition:` stmt) + if [ $ENABLE_PYTHON = 'OFF' ]; then exit 0; fi + pwd + ls -l + # find spack installed cmake + export ROOT_DIR=`pwd` + export CMAKE_BIN_DIR=`ls -d ${ROOT_DIR}/uberenv_libs/spack/opt/spack/*/*/cmake*/bin` + export PATH=${CMAKE_BIN_DIR}:$PATH + echo $PATH + # find spack installed python + export PYTHON_EXE=`ls -d ${ROOT_DIR}/uberenv_libs/spack/opt/spack/*/*/python*/bin/python` + # add conduit module to python path + export PYTHONPATH=${ROOT_DIR}/install/python-modules/ + echo $PYTHONPATH + which cmake + # lets build + cd install/examples/conduit/cpp_fort_and_py + mkdir _test_build + cd _test_build + cmake -DCONDUIT_DIR=${ROOT_DIR}/install -DPYTHON_EXECUTABLE=${PYTHON_EXE} ../ + make VERBOSE=1 + # lets run! + ./conduit_cpp_and_py_ex + ./conduit_fort_and_py_ex + displayName: 'Test vs Install (cpp_fort_and_py)' + + ############################################################################### # Docker build and test case, that leverages our script that calls Docker Build ############################################################################### diff --git a/src/docs/sphinx/conduit.rst b/src/docs/sphinx/conduit.rst index e201bfe01..6184f30ee 100644 --- a/src/docs/sphinx/conduit.rst +++ b/src/docs/sphinx/conduit.rst @@ -11,5 +11,6 @@ Conduit tutorial_cpp tutorial_python + tutorial_cpp_fort_and_py .. conduit_api diff --git a/src/examples/CMakeLists.txt b/src/examples/CMakeLists.txt index 9a035f783..2d1ba2ccd 100644 --- a/src/examples/CMakeLists.txt +++ b/src/examples/CMakeLists.txt @@ -11,5 +11,8 @@ add_subdirectory(staging) # install using and python examples -install(DIRECTORY using-with-cmake using-with-make python +install(DIRECTORY using-with-cmake + using-with-make + python + cpp_fort_and_py DESTINATION examples/conduit) diff --git a/src/examples/cpp_fort_and_py/CMakeLists.txt b/src/examples/cpp_fort_and_py/CMakeLists.txt new file mode 100644 index 000000000..2ca8773f2 --- /dev/null +++ b/src/examples/cpp_fort_and_py/CMakeLists.txt @@ -0,0 +1,106 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +# Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +# other details. No copyright assignment is required to contribute to Conduit. +############################################################################### +# +# Example that shows how to use Conduit across C++, Fortran, and an +# embedded Python interpreter. +# +# +# Building: +# +# Note: The python instance must have the conduit python module installed +# or it must be in your PYTHONPATH. +# +# > mkdir build +# > cd build +# +# # if conduit python module is not installed in your python instance +# # > export PYTHONPATH=/path/to/conduit-install/python-modules +# +# > cmake \ +# -DCONDUIT_DIR=/path/to/conduit/install +# -DPYTHON_EXECUTABLE=/path/to/python/bin/python +# ../ +# > make +# +# +# Running: +# > ./conduit_cpp_and_py_ex +# > ./conduit_fort_and_py_ex +# +# # if conduit python module is not installed in your python instance +# > env PYTHONPATH=/path/to/conduit-install/python-modules ./conduit_cpp_and_py_ex +# > env PYTHONPATH=/path/to/conduit-install/python-modules ./conduit_fort_and_py_ex +############################################################################### + +cmake_minimum_required(VERSION 3.0) + +project(conduit_cpp_fort_and_py C CXX Fortran) + + +###### +# Setup Conduit +###### +find_package(Conduit REQUIRED + NO_DEFAULT_PATH + PATHS ${CONDUIT_DIR}/lib/cmake/conduit) + +###### +# Setup Python +###### +include(SetupPython.cmake) + +###### +# If Conduit was built with c++11 support, make sure we enable it +# for our project. +###### +if(CONDUIT_USE_CXX11) + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + + +############################################################################## +# cpp to and from fortran example +############################################################################## +add_executable(conduit_cpp_and_py_ex + conduit_cpp_and_py_ex.cpp + python_interpreter.hpp + python_interpreter.cpp) + +target_link_libraries(conduit_cpp_and_py_ex + conduit::conduit + conduit::conduit_python) + + +# extra includes and libs to support the embedded python interpreter +target_include_directories(conduit_cpp_and_py_ex + PUBLIC ${PYTHON_INCLUDE_DIR}) + +target_link_libraries(conduit_cpp_and_py_ex + ${PYTHON_LIBRARY}) + +############################################################################## +# fortran to and from python example +############################################################################## + +add_executable(conduit_fort_and_py_ex + conduit_fort_and_py_ex.F90 + conduit_fort_and_py_mod.cpp + conduit_fort_and_py_mod.F90 + python_interpreter.hpp + python_interpreter.cpp) + + +target_link_libraries(conduit_fort_and_py_ex + conduit::conduit + conduit::conduit_python) + +# extra includes and libs to support the embedded python interpreter +target_include_directories(conduit_fort_and_py_ex + PUBLIC ${PYTHON_INCLUDE_DIR}) + +target_link_libraries(conduit_fort_and_py_ex + ${PYTHON_LIBRARY}) + diff --git a/src/examples/cpp_fort_and_py/SetupPython.cmake b/src/examples/cpp_fort_and_py/SetupPython.cmake new file mode 100644 index 000000000..89fdb5cf6 --- /dev/null +++ b/src/examples/cpp_fort_and_py/SetupPython.cmake @@ -0,0 +1,362 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +# Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +# other details. No copyright assignment is required to contribute to Conduit. + +# Find the interpreter first +if(PYTHON_DIR AND NOT PYTHON_EXECUTABLE) + set(PYTHON_EXECUTABLE ${PYTHON_DIR}/bin/python) +endif() + +find_package(PythonInterp REQUIRED) +if(PYTHONINTERP_FOUND) + + MESSAGE(STATUS "PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE}") + + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "import sys;from distutils.sysconfig import get_python_inc;sys.stdout.write(get_python_inc())" + OUTPUT_VARIABLE PYTHON_INCLUDE_DIR + ERROR_VARIABLE ERROR_FINDING_INCLUDES) + MESSAGE(STATUS "PYTHON_INCLUDE_DIR ${PYTHON_INCLUDE_DIR}") + + if(NOT EXISTS ${PYTHON_INCLUDE_DIR}) + MESSAGE(FATAL_ERROR "Reported PYTHON_INCLUDE_DIR ${PYTHON_INCLUDE_DIR} does not exist!") + endif() + + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "import sys;from distutils.sysconfig import get_python_lib;sys.stdout.write(get_python_lib())" + OUTPUT_VARIABLE PYTHON_SITE_PACKAGES_DIR + ERROR_VARIABLE ERROR_FINDING_SITE_PACKAGES_DIR) + MESSAGE(STATUS "PYTHON_SITE_PACKAGES_DIR ${PYTHON_SITE_PACKAGES_DIR}") + + if(NOT EXISTS ${PYTHON_SITE_PACKAGES_DIR}) + MESSAGE(FATAL_ERROR "Reported PYTHON_SITE_PACKAGES_DIR ${PYTHON_SITE_PACKAGES_DIR} does not exist!") + endif() + + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "import sys;from distutils.sysconfig import get_config_var; sys.stdout.write(get_config_var('LIBDIR'))" + OUTPUT_VARIABLE PYTHON_LIB_DIR + ERROR_VARIABLE ERROR_FINDING_LIB_DIR) + MESSAGE(STATUS "PYTHON_LIB_DIR ${PYTHON_LIB_DIR}") + + # if we are on macOS or linux, expect PYTHON_LIB_DIR to exist + # windows logic does not need PYTHON_LIB_DIR + if(NOT WIN32 AND NOT EXISTS ${PYTHON_LIB_DIR}) + MESSAGE(FATAL_ERROR "Reported PYTHON_LIB_DIR ${PYTHON_LIB_DIR} does not exist!") + endif() + + # check if we need "-undefined dynamic_lookup" by inspecting LDSHARED flags + execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" + "import sys;import sysconfig;sys.stdout.write(sysconfig.get_config_var('LDSHARED'))" + OUTPUT_VARIABLE PYTHON_LDSHARED_FLAGS + ERROR_VARIABLE ERROR_FINDING_PYTHON_LDSHARED_FLAGS) + + MESSAGE(STATUS "PYTHON_LDSHARED_FLAGS ${PYTHON_LDSHARED_FLAGS}") + + if(PYTHON_LDSHARED_FLAGS MATCHES "-undefined dynamic_lookup") + MESSAGE(STATUS "PYTHON_USE_UNDEFINED_DYNAMIC_LOOKUP_FLAG is ON") + set(PYTHON_USE_UNDEFINED_DYNAMIC_LOOKUP_FLAG ON) + else() + MESSAGE(STATUS "PYTHON_USE_UNDEFINED_DYNAMIC_LOOKUP_FLAG is OFF") + set(PYTHON_USE_UNDEFINED_DYNAMIC_LOOKUP_FLAG OFF) + endif() + + # check for python libs differs for windows python installs + if(NOT WIN32) + # we may build a shared python module against a static python + # check for both shared and static libs cases + + # check for shared first + set(PYTHON_GLOB_TEST "${PYTHON_LIB_DIR}/libpython*${CMAKE_SHARED_LIBRARY_SUFFIX}") + FILE(GLOB PYTHON_GLOB_RESULT ${PYTHON_GLOB_TEST}) + # then for static if shared is not found + if(NOT PYTHON_GLOB_RESULT) + set(PYTHON_GLOB_TEST "${PYTHON_LIB_DIR}/libpython*${CMAKE_STATIC_LIBRARY_SUFFIX}") + endif() + else() + if(PYTHON_LIB_DIR) + set(PYTHON_GLOB_TEST "${PYTHON_LIB_DIR}/python*.lib") + else() + get_filename_component(PYTHON_ROOT_DIR ${PYTHON_EXECUTABLE} DIRECTORY) + set(PYTHON_GLOB_TEST "${PYTHON_ROOT_DIR}/libs/python*.lib") + endif() + endif() + + FILE(GLOB PYTHON_GLOB_RESULT ${PYTHON_GLOB_TEST}) + + # make sure we found something + if(NOT PYTHON_GLOB_RESULT) + message(FATAL_ERROR "Failed to find main python library using pattern: ${PYTHON_GLOB_TEST}") + endif() + + if(NOT WIN32) + # life is ok on windows, but elsewhere + # the glob result might be a list due to symlinks, etc + # if it is a list, select the first entry as py lib + list(LENGTH PYTHON_GLOB_RESULT PYTHON_GLOB_RESULT_LEN) + if(${PYTHON_GLOB_RESULT_LEN} GREATER 1) + list(GET PYTHON_GLOB_RESULT 0 PYTHON_GLOB_RESULT) + endif() + endif() + + get_filename_component(PYTHON_LIBRARY "${PYTHON_GLOB_RESULT}" ABSOLUTE) + + MESSAGE(STATUS "{PythonLibs from PythonInterp} using: PYTHON_LIBRARY=${PYTHON_LIBRARY}") + find_package(PythonLibs) + + if(NOT PYTHONLIBS_FOUND) + MESSAGE(FATAL_ERROR "Failed to find Python Libraries using PYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}") + endif() + +endif() + + +find_package_handle_standard_args(Python DEFAULT_MSG + PYTHON_LIBRARY PYTHON_INCLUDE_DIR) + + + +############################################################################## +# Macro to use a pure python distutils setup script +############################################################################## +FUNCTION(PYTHON_ADD_DISTUTILS_SETUP) + set(singleValuedArgs NAME DEST_DIR PY_MODULE_DIR PY_SETUP_FILE FOLDER) + set(multiValuedArgs PY_SOURCES) + + ## parse the arguments to the macro + cmake_parse_arguments(args + "${options}" "${singleValuedArgs}" "${multiValuedArgs}" ${ARGN} ) + + # check req'd args + if(NOT DEFINED args_NAME) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument NAME") + endif() + + if(NOT DEFINED args_DEST_DIR) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument DEST_DIR") + endif() + + if(NOT DEFINED args_PY_MODULE_DIR) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_MODULE_DIR") + endif() + + if(NOT DEFINED args_PY_SETUP_FILE) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_SETUP_FILE") + endif() + + if(NOT DEFINED args_PY_SOURCES) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_SOURCES") + endif() + + MESSAGE(STATUS "Configuring python distutils setup: ${args_NAME}") + + # dest for build dir + set(abs_dest_path ${CMAKE_BINARY_DIR}/${args_DEST_DIR}) + if(WIN32) + # on windows, distutils seems to need standard "\" style paths + string(REGEX REPLACE "/" "\\\\" abs_dest_path ${abs_dest_path}) + endif() + + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${args_NAME}_build + COMMAND ${PYTHON_EXECUTABLE} ${args_PY_SETUP_FILE} -v + build + --build-base=${CMAKE_CURRENT_BINARY_DIR}/${args_NAME}_build + install + --install-purelib="${abs_dest_path}" + DEPENDS ${args_PY_SETUP_FILE} ${args_PY_SOURCES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + + add_custom_target(${args_NAME} ALL DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/${args_NAME}_build) + + # also use distutils for the install ... + # if PYTHON_MODULE_INSTALL_PREFIX is set, install there + if(PYTHON_MODULE_INSTALL_PREFIX) + set(py_mod_inst_prefix ${PYTHON_MODULE_INSTALL_PREFIX}) + # make sure windows style paths don't ruin our day (or night) + if(WIN32) + string(REGEX REPLACE "/" "\\\\" py_mod_inst_prefix ${PYTHON_MODULE_INSTALL_PREFIX}) + endif() + INSTALL(CODE + " + EXECUTE_PROCESS(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND ${PYTHON_EXECUTABLE} ${args_PY_SETUP_FILE} -v + build --build-base=${CMAKE_CURRENT_BINARY_DIR}/${args_NAME}_build_install + install --install-purelib=${py_mod_inst_prefix} + OUTPUT_VARIABLE PY_DIST_UTILS_INSTALL_OUT) + MESSAGE(STATUS \"\${PY_DIST_UTILS_INSTALL_OUT}\") + ") + else() + # else install to the dest dir under CMAKE_INSTALL_PREFIX + INSTALL(CODE + " + EXECUTE_PROCESS(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND ${PYTHON_EXECUTABLE} ${args_PY_SETUP_FILE} -v + build --build-base=${CMAKE_CURRENT_BINARY_DIR}/${args_NAME}_build_install + install --install-purelib=\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${args_DEST_DIR} + OUTPUT_VARIABLE PY_DIST_UTILS_INSTALL_OUT) + MESSAGE(STATUS \"\${PY_DIST_UTILS_INSTALL_OUT}\") + ") + endif() + + # set folder if passed + if(DEFINED args_FOLDER) + blt_set_target_folder(TARGET ${args_NAME} FOLDER ${args_FOLDER}) + endif() + +ENDFUNCTION(PYTHON_ADD_DISTUTILS_SETUP) + +############################################################################## +# Macro to create a compiled python module +############################################################################## +# +# we use this instead of the std ADD_PYTHON_MODULE cmake command +# to setup proper install targets. +# +############################################################################## +FUNCTION(PYTHON_ADD_COMPILED_MODULE) + set(singleValuedArgs NAME DEST_DIR PY_MODULE_DIR FOLDER) + set(multiValuedArgs SOURCES) + + ## parse the arguments to the macro + cmake_parse_arguments(args + "${options}" "${singleValuedArgs}" "${multiValuedArgs}" ${ARGN} ) + + # check req'd args + if(NOT DEFINED args_NAME) + message(FATAL_ERROR + "PYTHON_ADD_COMPILED_MODULE: Missing required argument NAME") + endif() + + if(NOT DEFINED args_DEST_DIR) + message(FATAL_ERROR + "PYTHON_ADD_COMPILED_MODULE: Missing required argument DEST_DIR") + endif() + + if(NOT DEFINED args_PY_MODULE_DIR) + message(FATAL_ERROR + "PYTHON_ADD_COMPILED_MODULE: Missing required argument PY_MODULE_DIR") + endif() + + if(NOT DEFINED args_SOURCES) + message(FATAL_ERROR + "PYTHON_ADD_COMPILED_MODULE: Missing required argument SOURCES") + endif() + + MESSAGE(STATUS "Configuring python module: ${args_NAME}") + PYTHON_ADD_MODULE(${args_NAME} ${args_SOURCES}) + + set_target_properties(${args_NAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY + ${CMAKE_BINARY_DIR}/${args_DEST_DIR}/${args_PY_MODULE_DIR}) + + # set folder if passed + if(DEFINED args_FOLDER) + blt_set_target_folder(TARGET ${args_NAME} FOLDER ${args_FOLDER}) + endif() + + foreach(CFG_TYPE ${CMAKE_CONFIGURATION_TYPES}) + string(TOUPPER ${CFG_TYPE} CFG_TYPE) + set_target_properties(${args_NAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY_${CFG_TYPE} + ${CMAKE_BINARY_DIR}/${args_DEST_DIR}/${args_PY_MODULE_DIR}) + endforeach() + + MESSAGE(STATUS "${args_NAME} build location: ${CMAKE_BINARY_DIR}/${args_DEST_DIR}/${args_PY_MODULE_DIR}") + + # macOS and linux + # defer linking with python, let the final python interpreter + # provide the proper symbols + + # on osx we need to use the following flag to + # avoid undefined linking errors + if(PYTHON_USE_UNDEFINED_DYNAMIC_LOOKUP_FLAG) + set_target_properties(${args_NAME} PROPERTIES + LINK_FLAGS "-undefined dynamic_lookup") + endif() + + # win32, link to python + if(WIN32) + target_link_libraries(${args_NAME} ${PYTHON_LIBRARIES}) + endif() + + # support installing the python module components to an + # an alternate dir, set via PYTHON_MODULE_INSTALL_PREFIX + set(py_install_dir ${args_DEST_DIR}) + if(PYTHON_MODULE_INSTALL_PREFIX) + set(py_install_dir ${PYTHON_MODULE_INSTALL_PREFIX}) + endif() + + install(TARGETS ${args_NAME} + EXPORT conduit + LIBRARY DESTINATION ${py_install_dir}/${args_PY_MODULE_DIR} + ARCHIVE DESTINATION ${py_install_dir}/${args_PY_MODULE_DIR} + RUNTIME DESTINATION ${py_install_dir}/${args_PY_MODULE_DIR} + ) + +ENDFUNCTION(PYTHON_ADD_COMPILED_MODULE) + +############################################################################## +# Macro to create a compiled distutils and compiled python module +############################################################################## +FUNCTION(PYTHON_ADD_HYBRID_MODULE) + set(singleValuedArgs NAME DEST_DIR PY_MODULE_DIR PY_SETUP_FILE FOLDER) + set(multiValuedArgs PY_SOURCES SOURCES) + + ## parse the arguments to the macro + cmake_parse_arguments(args + "${options}" "${singleValuedArgs}" "${multiValuedArgs}" ${ARGN} ) + + # check req'd args + if(NOT DEFINED args_NAME) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument NAME") + endif() + + if(NOT DEFINED args_DEST_DIR) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument DEST_DIR") + endif() + + if(NOT DEFINED args_PY_MODULE_DIR) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_MODULE_DIR") + endif() + + if(NOT DEFINED args_PY_SETUP_FILE) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_SETUP_FILE") + endif() + + if(NOT DEFINED args_PY_SOURCES) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument PY_SOURCES") + endif() + + if(NOT DEFINED args_SOURCES) + message(FATAL_ERROR + "PYTHON_ADD_HYBRID_MODULE: Missing required argument SOURCES") + endif() + + MESSAGE(STATUS "Configuring hybrid python module: ${args_NAME}") + + PYTHON_ADD_DISTUTILS_SETUP(NAME "${args_NAME}_py_setup" + DEST_DIR ${args_DEST_DIR} + PY_MODULE_DIR ${args_PY_MODULE_DIR} + PY_SETUP_FILE ${args_PY_SETUP_FILE} + PY_SOURCES ${args_PY_SOURCES} + FOLDER ${args_FOLDER}) + + PYTHON_ADD_COMPILED_MODULE(NAME ${args_NAME} + DEST_DIR ${args_DEST_DIR} + PY_MODULE_DIR ${args_PY_MODULE_DIR} + SOURCES ${args_SOURCES} + FOLDER ${args_FOLDER}) + +ENDFUNCTION(PYTHON_ADD_HYBRID_MODULE) + + diff --git a/src/examples/cpp_fort_and_py/conduit_cpp_and_py_ex.cpp b/src/examples/cpp_fort_and_py/conduit_cpp_and_py_ex.cpp new file mode 100644 index 000000000..8cf665235 --- /dev/null +++ b/src/examples/cpp_fort_and_py/conduit_cpp_and_py_ex.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +// Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +// other details. No copyright assignment is required to contribute to Conduit. + +//----------------------------------------------------------------------------- +/// +/// file: conduit_cpp_and_fort_ex.cpp +/// +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Demos creating and passing Conduit Nodes between C++ and Python +//----------------------------------------------------------------------------- + +#include + +#include "conduit.hpp" +#include "conduit_relay.hpp" +#include "conduit_blueprint.hpp" + +#include "python_interpreter.hpp" +// conduit python module capi header +#include "conduit_python.hpp" + + +// single python interp instance for our example. +PythonInterpreter *interp = NULL; + +// returns our static instance of our python interpreter +// if not already inited initializes it +PythonInterpreter *init_python_interpreter() +{ + if( interp == NULL) + { + interp = new PythonInterpreter(); + if( !interp->initialize() ) + { + std::cout << "ERROR: interp->initialize() failed " << std::endl; + return NULL; + } + // setup for conduit python c api + if(!interp->run_script("import conduit")) + { + std::cout << "ERROR: `import conduit` failed" << std::endl; + return NULL; + } + + if(import_conduit() < 0) + { + std::cout << "failed to import Conduit Python C-API"; + return NULL; + } + + // // Turn this on if you want to see every line + // // the python interpreter executes + //interp->set_echo(true); + } + return interp; +} + + +int main(int argc, char **argv) +{ + + PythonInterpreter *pyintp = init_python_interpreter(); + + // create a python interp, wrap conduit node into there + + conduit::Node n; + n["values"] = { 1.0,2.0,3.0,4.0}; + n["shape"] = {2,2}; + + std::cout << "Hello from C++, here is the Node we created:" << std::endl; + n.print(); + + // create py object to wrap the conduit node + PyObject *py_node = PyConduit_Node_Python_Wrap(&n, + 0); // python owns => false + + // get global dict and insert wrapped conduit node + PyObject *py_mod_dict = pyintp->global_dict(); + + pyintp->set_dict_object(py_mod_dict, + py_node, + "my_node"); + + // + // NOTE: we aren't checking pyintp->run_script return to simplify + // this example -- but you should check in real cases! + // + + // access python in cpp + pyintp->run_script("print(my_node)"); + pyintp->run_script("import numpy"); + pyintp->run_script("vals_view = my_node['values'].reshape(my_node['shape'])"); + pyintp->run_script("print('Hello from Python, here is the vals_view')"); + pyintp->run_script("print(vals_view)"); + + + std::string py_name = "my_py_node"; + + // create a node in python, and access it in c++ + std::ostringstream oss; + oss << py_name << " = conduit.Node()" << std::endl + << py_name << "['values'] = [10.0,20.0,30.0,40.0]" << std::endl + << py_name << "['shape'] = [2,2]" << std::endl + << "print('Hello from python, I created:')" << std::endl + << "print(" << py_name << ")" << std::endl; + + pyintp->run_script(oss.str()); + + // fetch wrapped conduit node + PyObject *py_obj = pyintp->get_dict_object(py_mod_dict, + py_name); + + if(!PyConduit_Node_Check(py_obj)) + { + // error! + } + + conduit::Node *cpp_res = PyConduit_Node_Get_Node_Ptr(py_obj); + + std::cout << "Hello from C++, here is the Node we got from python:" << std::endl; + cpp_res->print(); + +} + + diff --git a/src/examples/cpp_fort_and_py/conduit_fort_and_py_ex.F90 b/src/examples/cpp_fort_and_py/conduit_fort_and_py_ex.F90 new file mode 100644 index 000000000..780988b91 --- /dev/null +++ b/src/examples/cpp_fort_and_py/conduit_fort_and_py_ex.F90 @@ -0,0 +1,56 @@ +! Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +! Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +! other details. No copyright assignment is required to contribute to Conduit. + +!----------------------------------------------------------------------------- +! +! file: conduit_fort_and_py_ex.F90 +! +!----------------------------------------------------------------------------- + +!----------------------------------------------------------------------------- +! Demos creating and passing Conduit Nodes between Fortran and Python +!----------------------------------------------------------------------------- + +PROGRAM main + use iso_c_binding + ! use the conduit fortran interface + use conduit + ! use our example module + use conduit_fort_and_py_mod + implicit none + + type(C_PTR) cnode, cnodepy + real(4), dimension(4) :: my_data + integer(4), dimension(2) :: my_shape + integer i + + ! fill our 32-bit 4 integer array + do i = 1,4 + my_data(i) = i + enddo + ! set our shape + my_shape(1) = 2 + my_shape(2) = 2 + + cnode = conduit_node_create() + call conduit_node_set_path_float32_ptr(cnode,"values",my_data, 4_8) + call conduit_node_set_path_int32_ptr(cnode,"shape",my_shape, 2_8) + + print*,"Hello from Fortran, here is the Node we created:" + call conduit_node_print(cnode) + + ! pass this node off to python + call conduit_fort_to_py(cnode) + + ! lets create a node in python and access it in fortran + cnodepy = conduit_fort_from_py("my_py_node") + + print*,"Hello from Fortran, here is the Node we got from python:" + ! print our objc from python + call conduit_node_print(cnodepy) + + ! we own this one so clean it up. + call conduit_node_destroy(cnode) + +END PROGRAM main diff --git a/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.F90 b/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.F90 new file mode 100644 index 000000000..fb367edfc --- /dev/null +++ b/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.F90 @@ -0,0 +1,61 @@ +! Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +! Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +! other details. No copyright assignment is required to contribute to Conduit. + +!----------------------------------------------------------------------------- +! +! file: conduit_fort_and_py_mod.F90 +! +!/----------------------------------------------------------------------------- + + +!----------------------------------------------------------------------------- +! +! fortran binding interface for conduit_fort_and_py_mod module +! +!----------------------------------------------------------------------------- + +module conduit_fort_and_py_mod +!------------------------------------------------------------------------------ + use, intrinsic :: iso_c_binding, only : C_PTR + implicit none + + interface +!-------------------------------------------------------------------------- + subroutine conduit_fort_to_py(cnode) & + bind(C, name="conduit_fort_to_py") + use iso_c_binding + implicit none + type(C_PTR), value, intent(IN) :: cnode + end subroutine conduit_fort_to_py + + !-------------------------------------------------------------------------- + function c_conduit_fort_from_py(name) result(res) & + bind(C, name="conduit_fort_from_py") + use iso_c_binding + implicit none + character(kind=C_CHAR), intent(IN) :: name(*) + type(C_PTR) :: res + end function c_conduit_fort_from_py + + + end interface + +contains + + !-------------------------------------------------------------------------- + ! Note this method exists to apply fortran trim to passed string + ! before passing to the c api + !-------------------------------------------------------------------------- + function conduit_fort_from_py(name) result(res) + use iso_c_binding + implicit none + character(*), intent(IN) :: name + type(C_PTR) :: res + !--- + res = c_conduit_fort_from_py(trim(name) // C_NULL_CHAR) + end function conduit_fort_from_py + +!------------------------------------------------------------------------------ +end module conduit_fort_and_py_mod +!------------------------------------------------------------------------------ diff --git a/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.cpp b/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.cpp new file mode 100644 index 000000000..6f12c1c2a --- /dev/null +++ b/src/examples/cpp_fort_and_py/conduit_fort_and_py_mod.cpp @@ -0,0 +1,139 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +// Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +// other details. No copyright assignment is required to contribute to Conduit. + +//----------------------------------------------------------------------------- +/// +/// file: conduit_fort_and_py_mod.cpp +/// + +//----------------------------------------------------------------------------- +// +// c functions that we bind to create the fortran conduit_fort_and_py_mod +// +//----------------------------------------------------------------------------- + +#include +#include + +// conduit python module capi header +#include "conduit_python.hpp" + +// embedded interp +#include "python_interpreter.hpp" + +//----------------------------------------------------------------------------- +// -- begin extern C +//----------------------------------------------------------------------------- + + +// single python interp instance for our example module. +PythonInterpreter *interp = NULL; + +extern "C" { + +// returns our static instance of our python interpreter +// if not already inited initializes it +PythonInterpreter *init_python_interpreter() +{ + if( interp == NULL) + { + interp = new PythonInterpreter(); + if( !interp->initialize() ) + { + std::cout << "ERROR: interp->initialize() failed " << std::endl; + return NULL; + } + // setup for conduit python c api + if(!interp->run_script("import conduit")) + { + std::cout << "ERROR: `import conduit` failed" << std::endl; + return NULL; + } + + if(import_conduit() < 0) + { + std::cout << "failed to import Conduit Python C-API"; + return NULL; + } + + // // Turn this on if you want to see every line + // // the python interpreter executes + //interp->set_echo(true); + } + return interp; +} + +//----------------------------------------------------------------------------- +// access node passed from fortran to python +void +conduit_fort_to_py(conduit_node *data) +{ + PythonInterpreter *pyintp = init_python_interpreter(); + + // get cpp ref to passed node + conduit::Node &n = conduit::cpp_node_ref(data); + + // create py object to wrap the conduit node + PyObject *py_node = PyConduit_Node_Python_Wrap(&n, + 0); // python owns => false + + // get global dict and insert wrapped conduit node + PyObject *py_mod_dict = pyintp->global_dict(); + + pyintp->set_dict_object(py_mod_dict, + py_node, + "my_node"); + + // + // NOTE: we aren't checking pyintp->run_script return to simplify + // this example -- but you should check in real cases! + // + + pyintp->run_script("print('Hello from Python, here is what you passed:')"); + pyintp->run_script("print(my_node)"); + pyintp->run_script("vals_view = my_node['values'].reshape(my_node['shape'])"); + pyintp->run_script("print(vals_view)"); +} + +//----------------------------------------------------------------------------- +// create a node in python and return it for access in fortran +conduit_node * +conduit_fort_from_py(const char *py_name) +{ + PythonInterpreter *pyintp = init_python_interpreter(); + + std::ostringstream oss; + oss << py_name << " = conduit.Node()" << std::endl + << py_name << "['values'] = [10.0,20.0,30.0,40.0]" << std::endl + << py_name << "['shape'] = [2,2]" << std::endl + << "print('Hello from python, I created:')" << std::endl + << "print(" << py_name << ")" << std::endl; + + // + // NOTE: we aren't checking pyintp->run_script return to simplify + // this example -- but you should check in real cases! + // + + pyintp->run_script(oss.str()); + + // get global dict and fetch wrapped conduit node + PyObject *py_mod_dict = pyintp->global_dict(); + + PyObject *py_obj = pyintp->get_dict_object(py_mod_dict, + py_name); + + if(!PyConduit_Node_Check(py_obj)) + { + // error! + } + + conduit::Node *cpp_res = PyConduit_Node_Get_Node_Ptr(py_obj); + // return the c pointer + return conduit::c_node(cpp_res); +} + +} +//----------------------------------------------------------------------------- +// -- end extern C +//----------------------------------------------------------------------------- diff --git a/src/examples/cpp_fort_and_py/python_interpreter.cpp b/src/examples/cpp_fort_and_py/python_interpreter.cpp new file mode 100644 index 000000000..373387465 --- /dev/null +++ b/src/examples/cpp_fort_and_py/python_interpreter.cpp @@ -0,0 +1,867 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +// Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +// other details. No copyright assignment is required to contribute to Conduit. + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// +// Copyright (c) 2015-2021, Lawrence Livermore National Security, LLC. +// +// Produced at the Lawrence Livermore National Laboratory +// +// LLNL-CODE-716457 +// +// All rights reserved. +// +// This file is part of Ascent. +// +// For details, see: http://ascent.readthedocs.io/. +// +// Please also read ascent/LICENSE +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the disclaimer below. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the disclaimer (as noted below) in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the LLNS/LLNL nor the names of its contributors may +// be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, +// LLC, THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +// IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// + + +//----------------------------------------------------------------------------- +/// +/// file: python_interpreter.cpp +/// +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +/// +/// Simple C++ Embeddable Python Interpreter. +/// +/// ADAPTED FROM https://github.com/Alpine-DAV/ascent/tree/develop/src/flow +//----------------------------------------------------------------------------- + +#include "python_interpreter.hpp" + +// standard lib includes +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + + + +#if PY_MAJOR_VERSION >= 3 +#define IS_PY3K +#endif + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Begin Functions to help with Python 2/3 Compatibility. +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#if defined(IS_PY3K) + +//----------------------------------------------------------------------------- +int +PyString_Check(PyObject *o) +{ + return PyUnicode_Check(o); +} + + +//----------------------------------------------------------------------------- +char * +PyString_AsString(PyObject *py_obj) +{ + char *res = NULL; + if(PyUnicode_Check(py_obj)) + { + PyObject * temp_bytes = PyUnicode_AsEncodedString(py_obj, + "ASCII", + "strict"); // Owned reference + if(temp_bytes != NULL) + { + res = strdup(PyBytes_AS_STRING(temp_bytes)); + Py_DECREF(temp_bytes); + } + else + { + // TODO: Error + } + } + else if(PyBytes_Check(py_obj)) + { + res = strdup(PyBytes_AS_STRING(py_obj)); + } + else + { + // TODO: ERROR or auto convert? + } + + return res; +} + +//----------------------------------------------------------------------------- +PyObject * +PyString_FromString(const char *s) +{ + return PyUnicode_FromString(s); +} + +//----------------------------------------------------------------------------- +void +PyString_AsString_Cleanup(char *bytes) +{ + free(bytes); +} + +//----------------------------------------------------------------------------- +int +PyInt_Check(PyObject *o) +{ + return PyLong_Check(o); +} + +//----------------------------------------------------------------------------- +long +PyInt_AsLong(PyObject *o) +{ + return PyLong_AsLong(o); +} + +//----------------------------------------------------------------------------- +long +PyInt_AS_LONG(PyObject *o) +{ + return PyLong_AS_LONG(o); +} + +//----------------------------------------------------------------------------- +PyObject * +PyNumber_Int(PyObject *o) +{ + return PyNumber_Long(o); +} + + +#else // python 2.6+ + +//----------------------------------------------------------------------------- +#define PyString_AsString_Cleanup(c) { /* noop */ } + +#endif + +// helper for both python 2 and 3 +//----------------------------------------------------------------------------- +void +PyString_To_CPP_String(PyObject *py_obj, std::string &res) +{ + + char *str = PyString_AsString(py_obj); + res = str; + PyString_AsString_Cleanup(str); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// End Functions to help with Python 2/3 Compatibility. +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +/// +/// PythonInterpreter Constructor +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +PythonInterpreter::PythonInterpreter() +{ + m_handled_init = false; + m_running = false; + m_error = false; + m_echo = false; + + m_py_main_module = NULL; + m_py_global_dict = NULL; + + m_py_trace_module = NULL; + m_py_sio_module = NULL; + m_py_trace_print_exception_func = NULL; + m_py_sio_class = NULL; + +} + +//----------------------------------------------------------------------------- +/// +/// PythonInterpreter Destructor +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +PythonInterpreter::~PythonInterpreter() +{ + // Shutdown the interpreter if running. + shutdown(); + } + + +//----------------------------------------------------------------------------- +/// +/// PythonInterpreter::set_program_name +/// +//----------------------------------------------------------------------------- +void +PythonInterpreter::set_program_name(const char *prog_name) +{ +#ifdef IS_PY3K + wchar_t *w_prog_name = Py_DecodeLocale(prog_name, NULL); + Py_SetProgramName(w_prog_name); + PyMem_RawFree(w_prog_name); +#else + Py_SetProgramName(const_cast(prog_name)); +#endif +} + + +//----------------------------------------------------------------------------- +/// +/// PythonInterpreter::set_argv +/// +//----------------------------------------------------------------------------- +void +PythonInterpreter::set_argv(int argc, char **argv) +{ +#ifdef IS_PY3K + // alloc ptrs for encoded ver + std::vector wargv(argc); + + for(int i = 0; i < argc; i++) + { + wargv[i] = Py_DecodeLocale(argv[i], NULL); + } + + PySys_SetArgv(argc,&wargv[0]); + + for(int i = 0; i < argc; i++) + { + PyMem_RawFree(wargv[i]); + } + +#else + PySys_SetArgv(argc, argv); +#endif +} + +//----------------------------------------------------------------------------- +/// +/// Starts the python interpreter. If no arguments are passed creates +/// suitable dummy arguments +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::initialize(int argc, char **argv) +{ + // if already running, ignore + if(m_running) + return true; + + // Check Py_IsInitialized(), some one else may have inited python + if(Py_IsInitialized()) + { + // make sure we know we don't need to clean up the interp + m_handled_init = false; + } + else + { + // set prog name + const char *prog_name = "flow_embedded_py"; + + if(argc == 0 || argv == NULL) + { + set_program_name(prog_name); + } + else + { + set_program_name(argv[0]); + } + + // Init Python + Py_Initialize(); + PyEval_InitThreads(); + + // set sys argvs + + if(argc == 0 || argv == NULL) + { + set_argv(1, const_cast(&prog_name)); + } + else + { + set_argv(argc, argv); + } + + // make sure we know we need to cleanup the interp + m_handled_init = true; + } + + // do to setup b/c we need for c++ connection, + // even if python was already inited + + // setup up __main__ and capture StdErr + PyRun_SimpleString("import os,sys\n"); + if(check_error()) + return false; + + // all of these PyObject*s are borrowed refs + m_py_main_module = PyImport_AddModule((char*)"__main__"); + + if(m_py_main_module == NULL) + { + std::cout << "PythonInterpreter failed to import `__main__` module" << std::endl; + return false; + } + + m_py_global_dict = PyModule_GetDict(m_py_main_module); + + if(m_py_global_dict == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to access `__main__` dictionary"); + return false; + } + + // get objects that help us print exceptions + + + PyRun_SimpleString("import traceback\n"); + if(check_error()) + return false; + + // get ref to traceback.print_exception method + m_py_trace_module = PyImport_AddModule("traceback"); + + if(m_py_trace_module == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to import `traceback` module"); + return false; + } + + PyObject *py_trace_dict = PyModule_GetDict(m_py_trace_module); + + if(py_trace_dict == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to access `traceback` dictionary"); + return false; + } + + m_py_trace_print_exception_func = PyDict_GetItemString(py_trace_dict, + "print_exception"); + + if(m_py_trace_print_exception_func == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to access `print_exception` function"); + return false; + } + + // get ref to StringIO class + +#ifdef IS_PY3K + const char *sio_module_name = "io"; + PyRun_SimpleString("import io\n"); + if(check_error()) + return false; +#else + const char *sio_module_name = "StringIO"; + PyRun_SimpleString("import StringIO\n"); + if(check_error()) + return false; +#endif + + m_py_sio_module = PyImport_ImportModule(sio_module_name); + + if(m_py_sio_module == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to import " + << "`" + << sio_module_name + << "` module"); + return false; + } + + PyObject *py_sio_dict = PyModule_GetDict(m_py_sio_module); + + if(py_sio_dict == NULL) + { + CONDUIT_INFO("PythonInterpreter failed to access `" + << sio_module_name + << "` dictionary"); + return false; + } + + // input the class + m_py_sio_class = PyDict_GetItemString(py_sio_dict,"StringIO"); + + + if(m_py_sio_class == NULL) + { + CONDUIT_INFO("PythonInterpreter failed access StringIO class"); + return false; + } + + m_running = true; + + return true; +} + + +//----------------------------------------------------------------------------- +/// +/// Resets the state of the interpreter if it is running +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +void +PythonInterpreter::reset() +{ + if(m_running) + { + // clean gloal dict. + PyDict_Clear(m_py_global_dict); + } +} + +//----------------------------------------------------------------------------- +/// +/// Shuts down the interpreter if it is running +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +void +PythonInterpreter::shutdown() +{ + if(m_running) + { + if(m_handled_init) + { + Py_Finalize(); + } + + m_running = false; + m_handled_init = false; + } +} + + +//----------------------------------------------------------------------------- +/// +/// Adds passed path to "sys.path" +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::add_system_path(const std::string &path) +{ + return run_script("sys.path.insert(1,r'" + path + "')\n"); +} + +//----------------------------------------------------------------------------- +/// +/// Executes passed python script in the interpreter +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::run_script(const std::string &script) +{ + return run_script(script, m_py_global_dict); +} + +//----------------------------------------------------------------------------- +/// +/// Executes passed python script in the interpreter +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::run_script_file(const std::string &fname) +{ + return run_script_file(fname, m_py_global_dict); +} + +//----------------------------------------------------------------------------- +/// +/// Executes passed python script in the interpreter +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::run_script(const std::string &script, + PyObject *py_dict) +{ + bool res = false; + if(m_running) + { + // show contents of the script via conduit info if echo option + // is enabled + if(m_echo) + { + CONDUIT_INFO("PythonInterpreter::run_script " << script); + } + + PyRun_String((char*)script.c_str(), + Py_file_input, + py_dict, + py_dict); + if(!check_error()) + res = true; + } + return res; +} + +//----------------------------------------------------------------------------- +/// +/// Executes passed python script in the interpreter +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::run_script_file(const std::string &fname, + PyObject *py_dict) +{ + ifstream ifs(fname.c_str()); + if(!ifs.is_open()) + { + CONDUIT_ERROR("PythonInterpreter::run_script_file " + " failed to open "<< fname); + return false; + } + string py_script((istreambuf_iterator(ifs)), + istreambuf_iterator()); + ifs.close(); + return run_script(py_script, py_dict); +} + + + +//----------------------------------------------------------------------------- +/// +/// Adds C python object to the global dictionary. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::set_global_object(PyObject *py_obj, + const string &py_name) +{ + return set_dict_object(m_py_global_dict, py_obj, py_name); +} + +//----------------------------------------------------------------------------- +/// +/// Get C python object from the global dictionary. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +PyObject * +PythonInterpreter::get_global_object(const string &py_name) +{ + return get_dict_object(m_py_global_dict, py_name); +} + + +//----------------------------------------------------------------------------- +/// +/// Adds C python object to the global dictionary. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::set_dict_object(PyObject *py_dict, + PyObject *py_obj, + const string &py_name) +{ + PyDict_SetItemString(py_dict, py_name.c_str(), py_obj); + return !check_error(); +} + +//----------------------------------------------------------------------------- +/// +/// Get C python object from the global dictionary. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +PyObject * +PythonInterpreter::get_dict_object(PyObject *py_dict, + const string &py_name) +{ + PyObject *res = PyDict_GetItemString(py_dict, py_name.c_str()); + if(check_error()) + res = NULL; + return res; +} + +//----------------------------------------------------------------------------- +/// +/// Checks python error state and constructs appropriate error message +/// if an error did occur. It can be used to check for errors in both +/// python scripts & calls to the C-API. The difference between these +/// to cases is the existence of a python traceback. +/// +/// Note: This method clears the python error state, but it will continue +/// to return "true" indicating an error until clear_error() is called. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::check_error() +{ + if(PyErr_Occurred()) + { + m_error = true; + m_error_msg = ""; + + string sval =""; + PyObject *py_etype; + PyObject *py_eval; + PyObject *py_etrace; + + PyErr_Fetch(&py_etype, &py_eval, &py_etrace); + + if(py_etype) + { + PyErr_NormalizeException(&py_etype, &py_eval, &py_etrace); + + if(PyObject_to_string(py_etype, sval)) + { + m_error_msg = sval; + } + + if(py_eval) + { + if(PyObject_to_string(py_eval, sval)) + { + m_error_msg += sval; + } + } + + if(py_etrace) + { + if(PyTraceback_to_string(py_etype, py_eval, py_etrace, sval)) + { + m_error_msg += "\n" + sval; + } + } + } + + PyErr_Restore(py_etype, py_eval, py_etrace); + PyErr_Clear(); + } + + return m_error; +} + +//----------------------------------------------------------------------------- +/// +/// Clears environment error flag and message. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +void +PythonInterpreter::clear_error() +{ + if(m_error) + { + m_error = false; + m_error_msg = ""; + } +} + +//----------------------------------------------------------------------------- +/// +/// Helper that converts a python object to a double. +/// Returns true if the conversion succeeds. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::PyObject_to_double(PyObject *py_obj, double &res) +{ + if(PyFloat_Check(py_obj)) + { + res = PyFloat_AS_DOUBLE(py_obj); + return true; + } + + if(PyInt_Check(py_obj)) + { + res = (double) PyInt_AS_LONG(py_obj); + return true; + } + + if(PyLong_Check(py_obj)) + { + res = PyLong_AsDouble(py_obj); + return true; + } + + if(PyNumber_Check(py_obj) != 1) + return false; + + PyObject *py_val = PyNumber_Float(py_obj); + if(py_val == NULL) + return false; + res = PyFloat_AS_DOUBLE(py_val); + Py_DECREF(py_val); + return true; +} + +//----------------------------------------------------------------------------- +/// +/// Helper that converts a python object to an int. +/// Returns true if the conversion succeeds. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::PyObject_to_int(PyObject *py_obj, int &res) +{ + if(PyInt_Check(py_obj)) + { + res = (int)PyInt_AS_LONG(py_obj); + return true; + } + + if(PyLong_Check(py_obj)) + { + res = (int)PyLong_AsLong(py_obj); + return true; + } + + if(PyNumber_Check(py_obj) != 1) + return false; + + PyObject *py_val = PyNumber_Int(py_obj); + + if(py_val == NULL) + return false; + res = (int) PyInt_AS_LONG(py_val); + Py_DECREF(py_val); + return true; +} + +//----------------------------------------------------------------------------- +/// +/// Helper that converts a python object to a C++ string. +/// Returns true if the conversion succeeds. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::PyObject_to_string(PyObject *py_obj, std::string &res) +{ + PyObject *py_obj_str = PyObject_Str(py_obj); + if(py_obj_str == NULL) + return false; + + PyString_To_CPP_String(py_obj_str,res); + Py_DECREF(py_obj_str); + return true; +} + + +//----------------------------------------------------------------------------- +/// +/// Helper to turns a python traceback into a human readable string. +/// +/// Note: Adapted from VisIt: src/avt/PythonFilters/PythonInterpreter.cpp +//----------------------------------------------------------------------------- +bool +PythonInterpreter::PyTraceback_to_string(PyObject *py_etype, + PyObject *py_eval, + PyObject *py_etrace, + std::string &res) +{ + if(!py_eval) + py_eval = Py_None; + + // we can only print traceback if we have fully + // inited the interpreter, since it uses imported helpers + if(!m_running) + { + return false; + } + + // create a StringIO object "buffer" to print traceback into. + PyObject *py_args = Py_BuildValue("()"); + PyObject *py_buffer = PyObject_CallObject(m_py_sio_class, py_args); + Py_DECREF(py_args); + + if(!py_buffer) + { + PyErr_Print(); + return false; + } + + // call traceback.print_tb(etrace,file=buffer) + PyObject *py_res = PyObject_CallFunction(m_py_trace_print_exception_func, + (char*)"OOOOO", + py_etype, + py_eval, + py_etrace, + Py_None, + py_buffer); + if(!py_res) + { + PyErr_Print(); + return false; + } + + // call buffer.getvalue() to get python string object + PyObject *py_str = PyObject_CallMethod(py_buffer,(char*)"getvalue",NULL); + + + if(!py_str) + { + PyErr_Print(); + return false; + } + + // convert python string object to std::string + PyString_To_CPP_String(py_str,res); + + Py_DECREF(py_buffer); + Py_DECREF(py_res); + Py_DECREF(py_str); + + return true; +} + + + + + diff --git a/src/examples/cpp_fort_and_py/python_interpreter.hpp b/src/examples/cpp_fort_and_py/python_interpreter.hpp new file mode 100644 index 000000000..981f98e52 --- /dev/null +++ b/src/examples/cpp_fort_and_py/python_interpreter.hpp @@ -0,0 +1,168 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and other Conduit +// Project developers. See top-level LICENSE AND COPYRIGHT files for dates and +// other details. No copyright assignment is required to contribute to Conduit. + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// +// Copyright (c) 2015-2021, Lawrence Livermore National Security, LLC. +// +// Produced at the Lawrence Livermore National Laboratory +// +// LLNL-CODE-716457 +// +// All rights reserved. +// +// This file is part of Ascent. +// +// For details, see: http://ascent.readthedocs.io/. +// +// Please also read ascent/LICENSE +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the disclaimer below. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the disclaimer (as noted below) in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the LLNS/LLNL nor the names of its contributors may +// be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, +// LLC, THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +// IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// + + +//----------------------------------------------------------------------------- +/// +/// file: python_interpreter.hpp +/// +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +/// +/// Simple C++ Embeddable Python Interpreter. +/// +/// ADAPTED FROM https://github.com/Alpine-DAV/ascent/tree/develop/src/flow +//----------------------------------------------------------------------------- + +#ifndef PYTHON_INTERPRETER_HPP +#define PYTHON_INTERPRETER_HPP + +#include +#include + +class PythonInterpreter +{ +public: + PythonInterpreter(); + virtual ~PythonInterpreter(); + + /// instance lifetime control + bool initialize(int argc=0, char **argv=NULL); + + bool is_running() { return m_running; } + + /// Note: blows away everything in the main dict + /// use with caution! + void reset(); + void shutdown(); + + /// echo (default = false) + /// when enabled, controls if contents of execd python + // scripts are echoed to conduit info + bool echo_enabled() const { return m_echo; } + /// change echo setting + void set_echo(bool value) { m_echo = value; } + + void set_program_name(const char *name); + void set_argv(int argc, char **argv); + + /// helper to add a system path to access new modules + bool add_system_path(const std::string &path); + + /// script exec + bool run_script(const std::string &script); + bool run_script_file(const std::string &fname); + + /// script exec in specific dict + bool run_script(const std::string &script, + PyObject *py_dict); + bool run_script_file(const std::string &fname, + PyObject *py_dict); + + /// set into global dict + bool set_global_object(PyObject *py_obj, + const std::string &name); + /// fetch from global dict, returns borrowed reference + PyObject *get_global_object(const std::string &name); + /// access global dict object + PyObject *global_dict() { return m_py_global_dict; } + + /// set into given dict + bool set_dict_object(PyObject *py_dict, + PyObject *py_obj, + const std::string &name); + /// fetch from given dict, returns borrowed reference + PyObject *get_dict_object(PyObject *py_dict, + const std::string &name); + + /// error checking + bool check_error(); + void clear_error(); + std::string error_message() const { return m_error_msg; } + + /// helpers to obtain values from basic objects + static bool PyObject_to_double(PyObject *py_obj, + double &res); + + static bool PyObject_to_string(PyObject *py_obj, + std::string &res); + + static bool PyObject_to_int(PyObject *py_obj, + int &res); + +private: + bool PyTraceback_to_string(PyObject *py_etype, + PyObject *py_eval, + PyObject *py_etrace, + std::string &res); + + bool m_handled_init; + bool m_running; + bool m_echo; + bool m_error; + std::string m_error_msg; + + PyObject *m_py_main_module; + PyObject *m_py_global_dict; + + PyObject *m_py_trace_module; + PyObject *m_py_sio_module; + PyObject *m_py_trace_print_exception_func; + PyObject *m_py_sio_class; + +}; + + + +#endif +//----------------------------------------------------------------------------- +// -- end header ifdef guard +//----------------------------------------------------------------------------- + +