【Learning CMake Cookbook】第三章--第一部分

【Learning CMake Cookbook】第三章--第一部分,第1张

Learning CMake Cookbook Chapter03 Part01
    • 检测python解释器
      • Find\.cmake文件在哪?
      • 如何使用find_package()命令找不在“系统标准位置”的包?
      • 嵌入执行一个python脚本文件而不是一条单一的python语句
    • 检测python库
    • 检测python模块和包

本部分与python相关~

检测python解释器

这部分直接在CmakeLists.txt中进行python代码的嵌入,没有C/C++相关的源码,如下所示:


cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES NONE)
# 这个命令要我们找到python解释器, 即:PythonInterp
find_package(PythonInterp REQUIRED)
// find_package()是一个用于发现和设置包的CMake命令,这些模块包含CMake命令,用于标识系统标准位置中的包
// 这里注意了!!!识别的是 “ 系统标准位置 ” 的包
// CMake模块文件称为 Find.cmake ,当调用 find_package() 时,模块中的命令将会运行。
// 即写find_package()命令的时候, 在系统标准位置处的 Find.cmake 文件将会执行
// 另外,查找模块还设置了一些有用的变量从而反应实际找到了什么,引入这个模块后,就可以在CMakeLists中使用这些变量
// 这些变量实际上是在Find.cmake文件中设置的
// 对于FindPythonInterp.cmake附带有如下CMake变量

# PYTHONINTERP_FOUND:是否找到解释器
# PYTHON_EXECUTABLE:Python解释器到可执行文件的路径
# PYTHON_VERSION_STRING:Python解释器的完整版本信息
# PYTHON_VERSION_MAJOR:Python解释器的主要版本号
# PYTHON_VERSION_MINOR:Python解释器的次要版本号
# PYTHON_VERSION_PATCH:Python解释器的补丁版本号


// 可以强制CMake,查找特定版本的包。例如,要求Python解释器的版本大于或等于2.7
// 使用 find_package(PythonInterp 2.7)
// find_package(PythonInterp REQUIRED)表示强制依赖关系,即找不到这个包CMake就会终止


// 但是我还有一个问题就是:如何知道Find.cmake的具体位置呢?
// Find.cmake又是如何查找相应的包是否存在呢?


// 前后文中都有说明:如果软件包没有安装在标准位置,CMake就无法定位它们, 但是用户可以使用-D选项传递一个目录给cmake
// 告诉CMake具体的查找位置,如:查找python指定:$ cmake -D PYTHON_EXECUTABLE=/custom/location/python ..
// !!!这里是一个重点,因为大多数情况下,我们都需要在自定义的目录进行库文件的部署,此时如果还想使用find_package命令
// 则需要使用这种方法进行验证!



// 这里相当于写了一个python脚本,执行一个python命令并返回其输出值

execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "print('Hello, world!')" // 这句话的含义就是调用输出一个python语句,
    // 后面是执行以上的python命令并且捕获它的结果值和输出值
    // 很明显,这里就相当于是在命令行执行一句 $ python '-c' "print('Hello, world!')"
    // 注意这里的引号和 -c 都是必要的!-c 表示后面将要执行的是一个python语句而非一个文件
    // 加引号也同样代表了将要执行的是一个字符串语句
  RESULT_VARIABLE _status // 将 RESULT_VARIABLE 的值赋值给我们定义的变量_status
  OUTPUT_VARIABLE _hello_world // 将 OUTPUT_VARIABLE 的值赋值给我们定义的变量_hello_world
  ERROR_QUIET
  OUTPUT_STRIP_TRAILING_WHITESPACE
  )

message(STATUS "RESULT_VARIABLE is: ${_status}") // 最后打印python的命令
message(STATUS "OUTPUT_VARIABLE is: ${_hello_world}")

// compare the "manual" messages with the following handy helper
include(CMakePrintHelpers)  // 这里引入调试工具包,否则下文无法调用cmake_print_variables这个API
cmake_print_variables(_status _hello_world)
// 这句话将打印:_status="0" ; _hello_world="Hello, world!" 
// 其实就是另一种打印方式,与以上的message语句功能重合

在写这个代码块的时候难免想到一些问题:

  1. find_package() 这个命令是通过调用 Find.cmake这个文件来间接实现的,那么这个文件到底在哪(我们可以先不管它做了什么)?
  2. 以上的文中提到:如果不是“系统标准位置”的包,直接使用find_package()会导致无法找到其目录。那么对于我们自定义安装的包,如何使用find_package()来找到它呢?
  3. 在这个文件中,我们通过嵌入一条python语句来实现其对应语句的功能,那么随之而来想到的就是:我们可不可以通过这种方式来内嵌一个完整的python文件来执行呢?
Find.cmake文件在哪?

如果cmake是默认安装,则其对应的Find.cmake文件都会被保存在 /usr/share/cmake-3.16/Modules 路径下,或是 usr/local/share/cmake-3.16/Modules 路径下。
另外,执行find_package()命令会优先从项目根目录(与CMakeList.txt同级目录)下的Module文件夹(如果有的话)中寻找对应的Find.cmake并执行,其次才会从刚刚提到的默认目录下进行搜索并执行~。

如何使用find_package()命令找不在“系统标准位置”的包?

我们知道,其实find_package()对应执行的Find.cmake是初始化了一些变量。比如以上提到的:

# PYTHONINTERP_FOUND:是否找到解释器
# PYTHON_EXECUTABLE:Python解释器到可执行文件的路径
# PYTHON_VERSION_STRING:Python解释器的完整版本信息
# PYTHON_VERSION_MAJOR:Python解释器的主要版本号
# PYTHON_VERSION_MINOR:Python解释器的次要版本号
# PYTHON_VERSION_PATCH:Python解释器的补丁版本号

我们可以通过直接给cmake传入这些参数来解决问题,从而不必使用find_package()命令,如下指定python解释器的位置(其实就是python命令可执行文件的位置):

$ cmake -D PYTHON_EXECUTABLE=/custom/location/python ..

但是这样显然不是我想要的答案,因为我们相当于绕开了find_package()指令,并没有执行Find.cmake这个文件。我想得到的解决是,如何引导Find.cmake到对应的自定义库安装位置,从而让文件自动设置这些变量???(pending:2022-04-26)

嵌入执行一个python脚本文件而不是一条单一的python语句

比较简单,修改上述对应部分如下:

 set(Py_File_Path "/home/ctrtemp/Documents/VS_Code_Prj/Learning/Learning_Cmake/Own_Learning/chapter-03/recipe-01/example/test.py")
// set(Py_File_Path "../test.py")
// 以上使用相对路径或绝对路径来指定要执行的python文件
execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} ${Py_File_Path}
  RESULT_VARIABLE _status 
  OUTPUT_VARIABLE _hello_world 
  ERROR_QUIET
  OUTPUT_STRIP_TRAILING_WHITESPACE
  )

在build文件夹下执行cmake … 将会得到和以上相同的结果

但我仍然有问题:这其实并不是在C++中内嵌了python代码,而是在代码编译阶段执行了python脚本文件,和我预想的“使用C++调用执行python”还有一定的偏差!(Pending:2022-04-26)

检测python库

我们先来看一下源文件的做法:

#include 
int main(int argc, char *argv[]) {
  Py_SetProgramName(argv[0]); /* optional but recommended */
  Py_Initialize();
  PyRun_SimpleString("from time import time,ctime\n"
                     "print('Today is', ctime(time()))\n");
  Py_Finalize();
  return 0;
}

其大概意思就是通过给PyRun_SimpleString()函数传一个字符串格式的python语句,并将其结果输出。注意一个点:这里的Python.h在编辑器(我的是VScode)中是会直接报错的。警告说找不到这个文件,但是执行cmak语句却可以生成对应的可执行文件,这是如何做到的呢?

其实很多工程文件都是如此!以下几点必须明确:

  1. 在代码编辑器中,是否给出这种警报一般是编辑器检索系统环境变量中的路径,如果头文件没有出现在环境变量指定的路径下,则会报错说找不到(一般对于C/C++来说,头文件对应的环境变量大多为:C_Path / C_INCLUDE_PATH / CPLUS_INCLUDE_PATH)
  2. 一般工程需要很多的依赖文件,包括头文件以及库文件,如果用户想部署工程,并通过当场修改环境变量以适应工程的话是非常繁琐的,并且将会使得整个系统变量变得繁杂(就因为一个工程做很大修改既麻烦又造成了污染!)
  3. 所以一般是通过编辑CMakeLists.txt的方式,动态地为工程指定其对应的依赖文件的路径,相当于为工程进行“定制”。

于是有以下的CMakeList.txt部分代码:


cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-02 LANGUAGES C)

set(CMAKE_C_STANDARD 99) // 执行C99标准
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_C_STANDARD_REQUIRED ON)

find_package(PythonInterp REQUIRED)

message(STATUS "${PYTHON_VERSION_MAJOR}")
message(STATUS "${PYTHON_VERSION_MINOR}") 
// 我们在这里进行打印一次!以此印证一些变量是通过findpackage来进行设置的
// 这里很有必要进行记录,以说明find_package的作用,并且,你应该适当的看一看findXXX.cmake的源文件
// 看看他们是如何寻找并改变变量的

find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)

add_executable(hello-embedded-python hello-embedded-python.c)

message(STATUS "${PYTHON_INCLUDE_DIRS}")
message(STATUS "${PYTHON_LIBRARIES}") 

target_include_directories(hello-embedded-python
  PRIVATE
    ${PYTHON_INCLUDE_DIRS}
  )

target_link_libraries(hello-embedded-python
  PRIVATE
    ${PYTHON_LIBRARIES}
  )

以上代码尤其注意关于message对应的语句,输出依赖文件对应的位置,显然,这些变量是被find_package()找到并设置的。同样的,在头文件以及库文件对应的那几个环境变量中,没有包含其对应的路径,这也正是之前在代码编辑器中报错的原因。也正因为是我们通过CmakeLists.txt设置了对应的变量,才使得我们可以在编译的过程中索引到对应依赖文件,成功生成并执行可执行文件。

检测python模块和包

之前提到的是在CMakeLists.txt中嵌入了python语句或者文件并执行,但并没有满足我们的实际需求:我们实际想做的是在C/C++中直接内嵌python代码或用C/C++调用python脚本文件执行。这一节就是解决我们之前的需求的,在C/C++中内嵌执行python。

首先我们来看C++代码部分,基本的代码已经做了相对应的注释说明:

#include 
#include 

using namespace std;

//注意这里我们要输入一些参数了
int main(int argc, char *argv[]) {
  PyObject *pName, *pModule, *pDict, *pFunc;
  PyObject *pArgs, *pValue;
  int i;

  if (argc < 3) { // 如果参数过少则出现这个警告
    fprintf(stderr, "Usage: pure-embedding pythonfile funcname [args]\n");
    return 1;
  }
  // 应该输入的是四个参数,包括
  // 1:所调用的文件名
  // 2:所调用文件中的函数名
  // 3/4:函数需要的两个传入的参数
  // 但实际上包括五个参数,其中第一个参数是可执行文件其本身
  // 故argc总比实际参数多一个

  // cout<<"argc = "<

  // cout<<"argv 0 = "<
  // cout<<"argv 1 = "<
  // cout<<"argv 2 = "<
  // cout<<"argv 3 = "<
  // cout<<"argv 4 = "<

  Py_Initialize();

  PyRun_SimpleString("import sys");//用这种字符串的形式在C++中内嵌运行python代码
  PyRun_SimpleString("sys.path.append(\".\")");

  pName = PyUnicode_DecodeFSDefault(argv[1]);//这里是python文件名
  /* Error checking of pName left out */

  pModule = PyImport_Import(pName);
  Py_DECREF(pName);

  if (pModule != NULL) {
    pFunc = PyObject_GetAttrString(pModule, argv[2]);//这里是调用函数名
    /* pFunc is a new reference */

    if (pFunc && PyCallable_Check(pFunc)) {
      pArgs = PyTuple_New(argc - 3);
      for (i = 0; i < argc - 3; ++i) {
        pValue = PyLong_FromLong(atoi(argv[i + 3]));//这里传入两个函数参数
        if (!pValue) {
          Py_DECREF(pArgs);
          Py_DECREF(pModule);
          fprintf(stderr, "Cannot convert argument\n");
          return 1;
        }
        /* pValue reference stolen here: */
        PyTuple_SetItem(pArgs, i, pValue);
      }
      pValue = PyObject_CallObject(pFunc, pArgs);
      Py_DECREF(pArgs);
      if (pValue != NULL) {
        printf("Result of call: %ld\n", PyLong_AsLong(pValue));
        Py_DECREF(pValue);
      } else {
        Py_DECREF(pFunc);
        Py_DECREF(pModule);
        PyErr_Print();
        fprintf(stderr, "Call failed\n");
        return 1;
      }
    } else {
      if (PyErr_Occurred())
        PyErr_Print();
      fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
    }
    Py_XDECREF(pFunc);
    Py_DECREF(pModule);
  } else {
    PyErr_Print();
    fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
    return 1;
  }
  Py_Finalize();
  return 0;
}

执行时输入的参数如下:

$ ./pure-embedding use_numpy print_ones 2 3

之后来看CMake_Lists.txt文件:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-03 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(PythonInterp REQUIRED) 
find_package(PythonLibs ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR} EXACT REQUIRED)

execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import re, numpy; print(re.compile('/__init__.py.*').sub('',numpy.__file__));"
    // 执行一个python语句 导入numpy,re模块, 并打印出numpy这个包的位置
  // 这次的脚本较长,并且对python的内部库进行了引入 ———— 引入了一些模块
  RESULT_VARIABLE _numpy_status  // 如果找到了numpy这个包, 则_numpy_status将被设置为一个整数,否则为错误的字符串
  OUTPUT_VARIABLE _numpy_location // 还是用两个变量对结果变量和输出变量进行了承接
  ERROR_QUIET
  OUTPUT_STRIP_TRAILING_WHITESPACE
  )


if(NOT _numpy_status) // 如果没有找到 _numpy_location 将包含Numpy的路径
  set(NumPy ${_numpy_location} CACHE STRING "Location of NumPy")
  // NumPy是个新变量,它的值是_numpy_location的字符串, 但是后面的 CACHE STRING 是什么意思呢?
endif()

// 获取 Numpy的模块版本
execute_process(
  COMMAND
    ${PYTHON_EXECUTABLE} "-c" "import numpy; print(numpy.__version__)"
  OUTPUT_VARIABLE _numpy_version 
  ERROR_QUIET
  OUTPUT_STRIP_TRAILING_WHITESPACE
  )
message(STATUS "curren numpy version = ${_numpy_version}")

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy
  FOUND_VAR NumPy_FOUND   // 设置Numpy_FOUND变量以及其他的输出信息
  REQUIRED_VARS NumPy
  VERSION_VAR _numpy_version
  )

add_executable(pure-embedding "")

target_sources(pure-embedding
  PRIVATE
    Py${PYTHON_VERSION_MAJOR}-pure-embedding.cpp
  )

target_include_directories(pure-embedding
  PRIVATE
    ${PYTHON_INCLUDE_DIRS}
  )

target_link_libraries(pure-embedding
  PRIVATE
    ${PYTHON_LIBRARIES}
  )

add_custom_command( # 这里没有理解?!
  OUTPUT
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
  COMMAND
    ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
                                          ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
  DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/use_numpy.py
  )

# make sure building pure-embedding triggers the above
# custom command
target_sources(pure-embedding
  PRIVATE
    ${CMAKE_CURRENT_BINARY_DIR}/use_numpy.py
  )

首先这一部分的重点在于我们要理解add_custom_command()这个语句:

  1. 语句允许我们在代码中执行某些用户自定义的命令,相当于直接在命令行执行语句块中的一些命令
  2. 关于参数方面:COMMAND后面加一个所要输出的命令
  3. 之后的参数比较繁杂这里暂时pending

(Pending 2022-04-26)
感觉这里要较长期间的pending了,遇到一个真正需要C/C++嵌入python代码的应用时,用到之后才能更快更好的进行理解提高。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/791176.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-05
下一篇 2022-05-05

发表评论

登录后才能评论

评论列表(0条)

保存