Jenkins pipeline 이용하기

: 이번에는 GitHub 프로젝트에 Jenkinsfile을 생성하고, 여기에 기술된 작업 절차들을 Jenkins에서 수행하도록 하는 방법으로 pipeline을 사용할 수 있습니다. 이전에 Web에서 Job들을 일일이 적용하는 작업들은 매우 번거로울 뿐 아니라, Job이 추가/변경되는 경우 Website의 해당 Item에 들어가 일일이 수정해야하므로 여간 번거로운 일이 아닐 수 없습니다. 이를 Jenkinsfile에 기술해 놓으면 수정 또한 commit을 통해 할 수 있으므로 개발자 친화적인? 작업이라 할 수 있습니다. 그럼 적용하는 방법을 살펴보겠습니다.

저는 이전의 Jenkins 시작하기에서와 같이 https://github.com/JaehongParkYS/calculator_CICD 이 프로젝트를 사용하였습니다.


Manage Plugin에 들어가 위 그림처럼 Pipeline: Declarative Agent API을 설치해 줍니다.
저는 declarative 문법을 사용할 것이므로 해당 플러그인을 설치하였습니다.


New item을 선택해 줍니다.


Pipeline을 선택해 주고 이름은 아무 이름이나 설정해 줍니다.



위 그림처럼 프로젝트 주소와 Git 주소를 설정해 주고 Credential은 이전에 만든 것을 동일하게 사용해 줍니다.
그리고 Script Path에 GitHub 프로젝트에 추가해 준 Jenkinsfile의 이름(저는 Jenkinsfile로 했습니다.)을 적어 줍니다.
Build할 branch는 모든 브랜치를 대상으로 할 것이므로 /으로 하였습니다.


Item을 정의하고 난 후 Build Now를 선택하면 위 그림과 같이 Pipeline 형태의 기술된 작업들이 수행되는 것을 볼 수 있습니다.


세부 항목을 선택해 해당 로그를 보면, Test 작업에서 수행한 작업들의 결과(저는 Gtest를 이용해 테스트를 수행해 보았습니다.)를 볼 수 있습니다.

제가 작성한 Jenkinsfile은

pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                sh '''
                echo "Start Build"
                g++ -o main main.cpp calculator.cpp -lgtest -lpthread
                '''
            }
        }
        stage('Test') {
            steps {
                sh '''
                echo "Start Test"
                ./main
                '''
            }
        }
        stage('Archive') {
            steps {
                sh '''
                echo "Start Archiving"
                mv ./main ./artifact/
                '''
                archiveArtifacts artifacts: 'artifact/*', fingerprint: true 
            }
        }
    }
}

입니다.
위 기술된 항목들에 대한 설명은 다음 페이지에서 이어나가겠습니다.

'CICD > Jenkins' 카테고리의 다른 글

Jenkins - 환경변수 사용  (0) 2021.02.24
Jenkins pipeline - Declarative pipeline 기술  (1) 2020.11.17
Jenkins - GitHub 연결(2)  (0) 2020.11.12
Jenkins - 젠킨스 설정(1)  (0) 2020.11.12

Github 프로젝트와 Jenkins 연동

해당 프로젝트를 진행하기 위하여 저는 다음의 주소에 있는 프로젝트를 사용하였습니다.

https://github.com/JaehongParkYS/calculator_CICD

 

  1. Global Tool Configuration

    : 여기서는 자신이 빌드할 프로젝트에서 필요한 환경(Maven, JDK, Gradle, Git, etc.)을 구성합니다. 저는 C/C++ 이므로 Git만 설정하면 되는데 이미 설정이 되어있어 Pass 합니다.

 

  1. Plugin 설정

    : 필요한 Plugin들을 설치합니다. Git, Github관련 Plugin들은 Install suggested plugins에 포함되어있어 추가적으로 설치할 필요는 없습니다.

저는 Docker Plugin을 설치해 주었습니다.

설치된 플러그인들은 다음의 페이지에서 확인할 수 있습니다.
http://localhost:8754/updateCenter/

 

  1. Github 설정

위 설명대로 들어온 후 해당 git에 접속할 수 있는 token을 발급해 줍니다.

본인의 의도에 맞게 token에 access 권한을 부여해 줍니다.

스크롤을 내려 GitHub 란에서 Add 버튼을 선택해 줍니다.

위 사진과 같이 설정해 주고, Secret 란에는 GitHub에서 발급한 token을 넣어줍니다.

위 사진과 같이 생성한 Credential을 선택해 주고 Connection test를 해 봅니다.

 

  1. Github 연결

위 사진과 같이 [New Item] - [Freestyle project]를 선택해 주고 아무 이름이나 넣어 줍니다.

다음 그림과 같이 GitHub project란에 프로젝트 명(https://github.com/JaehongParkYS/calculator_CICD)을%EC%9D%84) 넣어주고, Source Code Management란에 git 주소(https://github.com/JaehongParkYS/calculator_CICD.git)를%EB%A5%BC) 넣어줍니다. 그리고 Credential 란에는

ssh-keygen -t rsa -f 파일명 명령으로 ssh 키를 만들어, .pub 확장자가 없는 파일(private 키)의 내용을 넣어 줍니다.

그 후, 다음과 같이 Build 절차와 Post-build action을 정의해 줍니다.
저는 C/C++ 프로젝트이어서 간단하게 작성하였습니다. 저장을 눌러주면 다음과 같이 Item이 생성되었습니다.

다시 GitHub의 프로젝트로 넘어와서,

위에서 ssh-keygen -t rsa -f 파일명 명령으로 만든 public key를 위 사진과 같이 GitHub 계정에 등록시켜 줍니다. 이를 통해 Jenkins와 GitHub이 사전에 정의된 키를 이용해 통신을 가능하게 합니다.

GitHub 프로젝트의 [Settings] - [Webhooks]로 넘어가 [Add webhook]을 선택한 뒤 위와 같이${Jenkins의 주소}/github-webhook/을 넣어줍니다. 저는 public 주소가 없으므로 추후 ngrok을 이용해 수정해 줄 것입니다.

 

  1. Host(Windows10)와 Guest(Ubuntu-VirtualBox)를 포트포워딩 시키기

    제 PC는 Windows10이 설치되어있어, 개발 편의를 위해 VirtualBox에 Ubuntu20.04를 설치하였고, 이 Ubuntu에 모든 개발환경(Jenkins 포함)을 설정하였습니다.

    따라서 Jenkins 특성상 외부에서 제가 설정한 Jenkins 서버에 접속하여야 하므로, Host(Windows10)와 Guest(Ubuntu)의 특정 포트(8754)를 포트포워딩 시켰습니다.

다음 그림과 같이 HostPC의 loopback 주소(127.0.0.1)의 8754 포트를 GuestPC의 IP주소:8754 포트에 포트 포워딩 시켰습니다.


Ubuntu에서 IP 주소를 확인하기 위해서는 ifconfig 명령을 사용하면 되는데요, VirtualBox에서 설치하였다면 10.0.2.15로 고정되어 있습니다.

 

  1. ngrok을 이용해 Host PC의 특정 포트를 외부에서 접근가능하도록 public 주소 만들기

    ngrok은 다음의 주소에서 설치할 수 있습니다.

ngrok.com/download

 

ngrok - download

Running this command will add your authtoken to your ngrok.yml file. Connecting an account will list your open tunnels in the dashboard, give you longer tunnel timeouts, and more. Visit the dashboard to get your auth token.

ngrok.com

설치가 끝난 후, 저는 8754 포트를 Jenkins 접속 포트로 사용하고 있기 때문에 다음과 같이 ngrok에 명령어를 실행시켜주면


외부에서 접근할 수 있는 주소가 나타납니다.
한번 접속해 볼까요?


네, Jenkins로 포트포워딩 되어 접속이 잘 되네요.

 

그렇다면 위 GitHub 프로젝트의 [Settings] - [Webhooks]로 넘어가 [Add webhook] 다시 올바른 주소를 설정해 주도록 합시다. 그리고 나서 PR을 발생시켜보면,

위 그림과 같이 Jenkins의 Job이 생기고, PR에도 Jenkins 관련 내용이 보입니다. ^^

 

7. Build 결과물(artifact) 확인

Build 결과물은 jenkins 설치 경로에 따라서 다르므로 설치경로를 확인하는게 좋습니다.

default 경로는 /var/lib/jenkins/ 이므로 참고하시고, 해당 경로로 들어가보면 다음과 같이 빌드 결과물과, 해당 artifact를 실행해 볼 수 있습니다.

 

Jenkins 설치 (Ubuntu 20.04)

  1. JDK 설치

    apt-get update
    sudo apt-get install openjdk-8-jdk
  2. Jenkins 저장소 key 다운로드 후 sources.list에 다운로드 경로 추가 & Jenkins 다운로드

    wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
    
    echo deb http://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list
    
    sudo apt-get update
    
    sudo apt-get install jenkins
  3. Jenkins 서비스 시작

    sudo service jenkins restart

이 후 Jenkins의 HTTP PORT로 설정되어있는 포트(기본 8080)에 웹 브라우저를 이용해 접속합니다.

  • http://localhost:8080
    Jenkins의 HTTP PORT는 다음의 파일을 수정하여 바꿀수 있습니다.
  • /etc/default/jenkinsHTTP_PORT=8080

페이지에 접속 후 비밀번호를 입력하는 페이지가 나오는데 비밀번호는 아래의 명령어로 확인할 수 있습니다.

  • sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Install suggested plugins 을 선택하여 플러그인을 설치합니다

관리자 계정을 생성합니다.

Jenkins 접속 URL을 설정합니다.

설정을 성공적으로 끝마치면 다음과 같은 페이지를 볼 수 있습니다.

Seamless 모드란?

: Seamless의 단어가 가지는 의미는 끊김이 없는, 매끄러운 등의 의미를 가지고 있습니다. 마치 두 물체를 이어 붙였을 때 두 물체 사이의 유격이 느껴지지 않는 상태를 의미합니다. 이러한 seamless mode를 VirtualBox에서는 다음의 그림과 같은 기능을 제공함으로써 얻을 수 있습니다. 마치 Host PC(Window) 에서 Guest PC(Linux)를 동시에 지원하고 있는 것 처럼 말이죠.

 

WSL(Window Subsystem for Linux)를 Window에서 지원하고는 있지만, 실제 Linux 환경을 운영하는 것 보다는 개인적으로 조금 불편한 느낌이 있어 VirtualBox를 사용하고 있습니다.
WSL과 Virtual Box를 동시에 사용하면 crash issue도 있더군요... 한 번 crash가 발생하니 되돌리기 쉽지않은..ㅠㅠ

 

Seamless 모드를 적용하는 방법

:다음의 사진과 같이 적용하면 됩니다.

 

1. Seamless 모드를 사용할 때 기본적으로 설정되어있는 비디오 메모리의 크기가 충분하지 않아 렌더링을 하지 못하고 Black window가 보이는 현상이 있습니다. 따라서 다음의 설정을 통해 이를 방지할 수 있습니다.

 

2. Seamless 모드의 사용은 `HostKey` + `L` 조합으로 전환할 수 있습니다. terminal 하나를 띄운 채 seamless mode에 진입하면 아래와 같이 보이게 됩니다.

다시 기본 모드로 돌아가려면 `Hostkey` + `C` 조합으로 돌아갈 수 있습니다.

 

3. 위의 사진에서 보면 terminal 주위에 margin이 생기는 걸 볼 수 있습니다. 테두리의 여백을 없애기 위해서는 다음과 같은 절차로 없앨 수 있습니다.

3-1. `~/.config/gtk-3.0/gtk.css` 파일 생성

3-1. 생성된 파일에 아래의 내용 입력

decoration, decoration:backdrop {
    border-radius: 0;
    border-width: 0;
    box-shadow: none;
    margin: 1px;
}

 

위 설정을 마치면 아래의 그림과 같이 여백이 제거된 상태의 seamless 모드를 즐길 수 있습니다.

 

4. seamless mode 상단에 도구모음 표시

아래의 그림에서와 같이 seamless 모드 상단에 도구모음을 표시하기 위해서는

 

다음의 설정을 수행하면 됩니다.

 

 

+ HostKey 확인 및 변경하는 방법

1. [VirtualBox] - [파일] - [환경설정] 진입

2. [입력] - [가상머신] - [호스트 키 조합] 을 선택 후 원하는 키 입력

개요

: CMake는 플랫폼 중립적 언어로 빌드 프로세스는 CMakeLists.txt 파일에 기술한다. 이는 C/C++, 자바, 일부 스크립트 언어를 지원하며 Unix 시스템에서 실행될 때 기본 동작은 Make 기반 프레임워크(makefile과 여러개의 프레임워크 파일)를 생성한다.

기초 문법

: 다음의 쉬운 예제를 보며 기본 문법을 소개하겠다.

project (basic-syntax C)

cmake_minimum_required (VERSION 2.6)

set (name Jay)
set (addr Seoul)
message ("${name}, please give me home in ${addr}")

set_property (SOURCE add.c PROPERTY Author Jay)
set_property (SOURCE mult.c PROPERTY Author Ho)
get_property (author_name SOURCE add.c PROPERTY Author)
message ("The author of add.c is ${author_name}")

위에서와 같이,

  • 모든 명령은 space로 구분된 인자를 전달받는다: command (arg1 arg2 ...)
  • 두 개의 단어를 하나의 인수로 사용하고 싶다면, 따옴표(')를 사용해 하나의 인수로 지정한다.
  • project 명령은 빌드 시스템을 식별할 수 있는 고유한 이름을 정의한다. 이는 eclipse와 같이 프로젝트 이름이 필요한 네이티브 빌드 도구에서 사용된다. 또한 C/C++, Java처럼 사용할 프로그래밍 언어를 지정한다.
  • cmake_minimum_required 명령은 CMake 2.6 버전 이후부터 지원되는 명령을 사용하겠다는 의미이다.
  • set 명령은 변수와 그 값을 정의한다.
  • set_property 명령을 통해 속성 값을 설정한다. 이를 통해 파일에 값을 저장할 수 있고 빌드 시스템은 이 파일 이름을 기준으로 속성값을 관리하고 다른 명령에서 자유롭게 접근할 수 있다.
add_exectuable (calculator add sub mult calc)
  • 컴파일 명령을 생성하고 종속성 그래프에 파일 이름을 추가한다.
  • 여기서 확장자가 사용되지 않았는데, CMake는 확장자를 자동으로 붙여준다. 따라서 최종 실행 프로그램명은 calculator.exe가 된다.
add_library (math STATIC add sub mult)
add_executable (calculator calc)
target_link_libraries (calculator math)
  • add_library 명령에서 add.c, sub.c, mult.c를 컴파일해 정적 라이브러리를 생성한다.
  • add_executable, target_link_libraries 명령을 통해 calc.c파일을 컴파일한 후 math 라이브러리와 링크하여 calculator 프로그램을 생성한다.

기타

  • include_directories 명령을 통해 컴파일러에 헤더 파일 경로를 추가할 수 있다. 이는 gcc의 -I 옵션과 같다.
  • link_directories 명령을 통해 라이브러리 경로를 추가할 수 있다. 이는 gcc의 -L 명령과 같다.

컴파일 플래그 설정

: 빌드 specification에 어떤 타입의 결과물을 생성할지 지정하고, 사용할 컴파일 플래그를 설정한다.

  • set (CMAKE_BUILD_TYPE Debug) 명령을 통해 소스 레벨 디버깅 정보를 포함한 debug 빌드를 생성할 수 있다.
  • set_property (DIRECTORY PROPERTY COMPILE_DEFINITIONS TEST=1) 명령을 통해 현재 디렉토리에 있는 모든 C 파일을 컴파일 시 TEST 심볼을 정의하도록 한다.
  • set_property (SOURCE add.c PROPERTY COMPILE_DEFINITIONS QUICKADD=1) 명령을 통해 add.c를 컴파일 할 때 QUICKADD 심볼이 추가되게 한다.

외부 명령과 타겟 추가

add_custom_target 명령은 새로운 상위 레벨 타겟을 정의하고 실행될 순서를 지정한다. 이는 결과물을 생성하지 않고 파일의 갱신상태에 의존하지 않는다.

project (custom_target)
cmake_minimum_required (VERSION 2.6)

add_custom_target (print-city ALL
    COMMAND echo "Seoul is nice city")

add_custom_target (print-time
    COMMAND echo "SIt is now 8:45")

add_custom_target (print-day
    COMMAND echo "Today is Sunday")

add_dependencies (print-city print-time print-day)
  • add_custom_target (print-city ALL ... 에서 ALL 키워드는 개발자가 타겟을 명시하지 않을 때 print-citydefault build로 실행되게 정의한다.
  • add_dependencies 명령을 통해 print-city가 print-time, print-day에 종속된다는 것을 정의한다.

흐름제어

if (${my_var})
    message ("..")
else ()
endif()

if (NOT my_var) # 변수에 ${}를 사용하지 않아도 됨

if (${my_age} EQUAL 40) #다른 변수나 상수와 비교 가능

if (EXIST file.txt) # 파일의 존재여부 확인

if (file.txt IS_NEWER_THAN file2.txt) # 두 파일 간 생성순서 비교

macro (my_macro ARG1 ARG2 ARG3) # 매크로 정의
    message("${ARG1} ...")

my_macro(1 2 3) # 정의한 매크로 사용

일반적으로 사용되는 캐시 변수

: 새로운 캐시 변수를 정의할 수 있고, 아래의 값들을 기본값으로 초기화할 수도 있으며, 캐시 변수는 일반 변수와 같이 사용될 수 있다. 그리고 이 값을 set명령으로 변경할 수 있다.

  • CMAKE_AR, CMAKE_C_COMPILER, CMAKE_LINKER: 라이브러리를 묶어주는 아카이브 도구, C 컴파일러, 오브젝트 링커의 절대 경로이다.
  • CMKAE_MAKE_PROGRAM: /usr/bin/gmake와 같은 네이티브 빌드 도구의 절대 경로이다. 기본 설정 버전을 다른 버전으로 변경할 수 있다.
  • CMAKE_BUILD_TYPE: 생성하고 싶은 빌드 트리의 타입을 지정할 수 있다.
    • Debug: 생성되는 오브젝트 파일과 실행파일이 디버깅 정보를 포함하게 한다.
    • Release: 최종 실행 파일이 최적화되고 디버깅 정보를 포함하지 않게 된다.
    • RelWithDebInfo: 실행 파일이 최적화되지만, 디버깅 정보를 포함한다.
    • MinSizeRel: 실행 파일이 최소한의 메모리를 사용하게 한다.
  • CMAKE_C_FLAGS_*: 앞의 네 가지 빌드 타입에 따라 C 컴파일 옵션이 지정된다.
  • CMAKE_EXE_LINKER_FLAGS_*: 앞의 C 컴파일 옵션과 유사하게 각 빌드 타입에 따른 링커 옵션을 나타낸다.

'빌드&테스트도구 > GCC&Make&CMake' 카테고리의 다른 글

make - make에 대한 개요  (0) 2020.10.06
GCC - (4) 링커(ld)에 관하여  (0) 2020.09.13
GCC - (3) gcc의 각종 옵션  (0) 2020.09.09
GCC - (2) gcc의 세부 과정  (0) 2020.09.07
GCC - (1) C 컴파일 과정 개요  (0) 2020.09.02

LOG 출력 함수

: 프로젝트의 크기가 제법 되는 경우에는 개발과 추후 디버깅을 위해서 로그를 사용하는 일이 많다. 그렇지 않다면 개발 시 일일히 print 함수를 찍어보며 값을 확인하거나 GDB 툴을 이용해야만 한다. GDB를 이용하는 방법은 괜찮지만 printf 함수를 이용해 값을 일일히 찍어보는 것은 여간 귀찮은 일이 아닐 수 없다. 이러한 필요성에 따라 로그 함수를 작성하는 방법에 대해 알아보자.

자체 LOG 출력 함수 만들기

: 다음과 같이 LOG 출력 함수를 만들 수 있다.

#include <stdio.h>
#include <stdarg.h>

#define LOG(fmt, ...) print_log(__FILE__, __FUNCTION__, __LINE__, fmt, ##__VA_ARGS__)

void print_log(const char *file, const char *func, int line, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    printf("%s/%s(%d): ", file, func, line);
    printf(fmt, ap);

    va_end(ap);

    printf("\\n");
}
int main() {
    LOG("Hello");

    return 0;
}

그리고 그 실행결과는 다음과 같다.

jay@ ~/Desktop/tutorial/log $ ./main 
main.cc/main(17): Hello

Inside LOG 출력 함수

: 위의 LOG 출력 함수에 사용된 여러가지 사항들에 대해 알아보자.

  1. #define LOG(fmt, ...)... 이 가지는 의미
    • ...variadic function을 정의할 때 사용되는데, 이는 가변 갯수의 parameter가 들어올 수 있음을 의미한다.
    • 대표적인 variadic function이 printf로, printf("%d \\n", 1), printf("%d %d \\n", 1, 2)와 같이 함수의 parameter가 정해져 있지 않다.
  2. print_log(__FILE__, __FUNCTION__, __LINE__, fmt, ##__VA_ARGS__)__FILE__, __FUNCTION__, __LINE__ 이 가지는 의미
    • FILE과 같이 두 개의 연속된 underscore로 시작하는 변수들은 대부분 컴파일러에 pre-define 되어있는 값들이다.
    • 여기서 FILE은 해당 명령어가 호출되고 있는 파일의 이름, FUNCTION은 해당 명령어가 속해있는 함수의 이름, LINE은 해당 명령어가 해당 파일의 몇 번째 라인에 위치해 있는지를 나타낸다.
  3. print_log(__FILE__, __FUNCTION__, __LINE__, fmt, ##__VA_ARGS__)##__VA_ARGS__가 가지는 의미
    • __VA_ARGS는 앞의 define 문에서의 ...에 대치되는 문구이다.
    • 여기서 ##이 붙은 이유는, LOG("Hi %s", "Jay")가 아닌 LOG("Hi")를 호출하였을 때 ...에 해당하는 argument가 없기 때문에 컴파일 오류가 나는 것을 방지하기 위함이다.
  4. va_list, va_start, va_end 이란?
    • 위의 variadic function에서 parameter의 갯수는 제한이 없다. 따라서 parameter에 어떤 값들이 들어오는지는 미정인 상태이므로, parameter에 들어오는 값들을 순회하여야 할 필요가 있다.
    • 따라서 va_start를 이용해 순회하고자 하는 파라미터의 위치를 지정하고, va_start, va_end로 각각 순회 리스트를 생성, 종료시킨다.

Python2 에서 Python3로 변경시 유의해야 할 점

아래의 참고자료 중 Python2 to Python3 Porting Guide 를 보면 자세한 설명과 그 이유를 알 수 있으니 해당 문서를 참고하길 바랍니다.

C를 Python3로 포팅

: C에서 Python3로 포팅하는 절차는 크게 3가지로 구성된다.

  1. Modernization, 개발한 코드들이 Python2의 기능으로 마이그레이션 된다. 이 단계 이후에는 Python2.6 이상을 지원하게 된다.
  2. Porting, Python3도입 후 Python3 뿐만 아니라 Python2에 대한 호환성을 유지하도록 만든다.
  3. Cleanup, Python2에 대한 지원을 제거한다. 이 단계 후에는 Python3.3 이상의 버전만을 지원한다.

PyObject

: 모든 object 타입들은 이 PyObject 타입의 확장이라 할 수 있다. 이는 Python이 object를 다루기 위해 해당 object를 가리키는 포인터를 저장하는 구조라고 보면 된다. 이 PyObject는 원본 object의 reference count, 해당하는 object에 대한 type object에 대한 포인터를 가지고 있다.

Modernization

: PyObject의 멤버에 대한 사용에서 다음의 항목들의 사용에 주의하라.

From To 설명
obj->ob_type Py_YPE(obj)  
obj->ob_refcnt Py_REFCNT(obj)  
obj->ob_size Py_SIZE(obj)  
PyObject_HEAD_INIT(NULL) PyVarObject_HEAD_INIT(NULL, 0) type object의 초기화
PyClass_*, Py_Instance_* PyType_* 앞의 두 클래스는 Python3에서 삭제된 클래스, v2.2에서는 유효
PyCObject_* PyCapsule_* Python3.3에서 삭제된 타입

Porting

: modernize의 결과로 당신은 Python2의 가장 최신 feature를 사용하고 있을 것이다. 이제부터는 Python3를 동시에 지원할 수 있도록 할 것이다.

  1. #include <py3c.h>을 통해 헤더파일을 include하고 헤더 파일의 위치를 컴파일러에 알려줘라
  2. Python3에서의 특별한 점은 Python3에서는 human-readable한 string과 binary data를 나누어 놓았다는 점이다. 따라서 py3c 헤더파일을 이용할 때, native stringPyStr_* (PyStr_FromString, PyStr_Type, PyStr_Check 등)을 사용하고 binary dataPyBytes_* (PyBytes_FromString, PyBytes_Type, PyBytes_Check 등)을 사용해라.
String 종류 Py2 Py3 사용처
PyStr_* PyString_* PyUnicode_* Human-readable 텍스트
PyBytes_* PyString_* PyBytes_* Binary data
PyUnicode_* PyUnicode_* PyUnicode_* Unicode strings
PyString_* PyString_* 사용불가 unported code
  1. String의 길이를 return하기 위해서는 PyStr_AsUTF8AndSize()를 사용해라. Python3에서는 PyUnicode_AsUTF8AndSize()가 사용됨에 유의하라.
  2. Python3에서 string이 2 종류(human-readable, binary data)로 분리되었다면, int와 long은 하나의 타입으로 합쳐졌다. 다시말해, PyInt_*은 제거되고 PyLong_*만이 남았다. 이를 py3c 헤더파일에서는 에일리어싱으로 정의해 놓았다.
  3. Python3에서 PyFloat_FromString 함수는 2번째 인자를 없앴다. 이를 py3c 헤더파일에서는 재정의함으로써 Python2와 동시에 지원하고 있다.
  4. 모듈 초기화시 Python2에서는 void init<module_name> 함수를 사용하였으나, Python3에서는 PyObject *PyInit_<module_name>함수를 사용한다. 이를 동시에 지원하기 위해서는 MODULE_INIT_FUNC 매크로를 사용하라.

Cleanup

: Porting의 결과로 Python2와 Python3를 동시에 지원하고 있는 코드를 얻을 수 있다. 여기서는 C에서 Python2와 관련된 문법을 더이상 사용하지 않도록 하는 것이다.

py3c.h 헤더파일이 포함되어있다면 이를 제거하라.

From To 설명
PyStr_* PyUnicode_* PyStr_으로 시작하는 함수는 더이상 사용되지 않음
PyInt_* PyLong_* 더 이상 사용되지 않음
MODULE_INIT_FUNC(module_name) PyMODINIT_FUNC Py_Init_<module_name>(void)  
Py_TPFLAGS_HAVE_WEAKREFS
Py_TPFLAGS_HAVE_ITER
  단순 제거
return PY3C_RICHCMP Py_RETURN_RICHCOMPARE() 링크 참조
capsulethunk.h   사용중이라면 제거하라
#if !IS_PY3 BLOCK   해당 블럭에 있는 코드 제거

참고자료 

[Ofiicial] Embedding Python Application
Python/C API Reference Manual
Python/C API Unicode Objects and Codecs
Python/C API List Objects
Python2 to Python3 Porting Guide
NLTK install guide
stackoverflow - No module name
stackoverflow - Python.h: No such file or directory

Python 모듈을 C에서 import 하기

: 이 내용은 Python으로 개발된 모듈들을 C에서 자유자재로 이용하기 위해서 사용해야할 API 들과 그 전반에 대한 개발 가이드이다.

파일의 구성

: 이 Sample App은 크게 3개의 함수들로 구성하였다.

첫 번째로는, string을 인자로 받아 string을 반환하는 make_string 함수.
두 번째로는, int를 인자로 받아 int를 반환하는 make_int 함수.
마지막으로는, list 타입을 반환하는 make_list 함수이다.

파일의 구조는 다음과 같이 구성하였다.

jay@ ~/Desktop/tutorial/embedded-python $ tree .
.
├── bin
├── scripts
│   ├── __init__.py
│   └── my_module.py
└── src
    └── main.cpp

my_module.py 파일은 다음의 함수들로 구성된다.

def make_string(param):
    return "my_string: " + param

def make_int(param):
    return (1 + param)

def make_list():
    return ["1", "2", "3"]

main.cpp 파일은 다음의 함수들로 구성된다.

#include <Python.h>

void initialize(char* module_name, PyObject **pModule)
{
    Py_Initialize();

    PyObject *pName = PyUnicode_DecodeFSDefault(module_name);
    *pModule = PyImport_Import(pName);

    Py_DECREF(pName);
}

void finalize(PyObject *pModule)
{
    Py_DECREF(pModule);
    Py_FinalizeEx();
}

void make_string(PyObject *pModule, char *arg)
{
    PyObject *pFunc, *pArgs, *pValue;

    pFunc = PyObject_GetAttrString(pModule, "make_string");
    pArgs = PyTuple_New(1);

    PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(arg));
    pValue = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);

    printf("Result of call: %s \n", PyUnicode_AsUTF8(pValue));
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
}


void make_int(PyObject *pModule, int arg)
{
    PyObject *pFunc, *pArgs, *pValue;

    pFunc = PyObject_GetAttrString(pModule, "make_int");
    pArgs = PyTuple_New(1);

    PyTuple_SetItem(pArgs, 0, PyLong_FromLong(arg));
    pValue = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs);

    printf("Result of call: %d \n", PyLong_AsLong(pValue));
    Py_DECREF(pValue);
    Py_DECREF(pFunc);

}

void make_list(PyObject *pModule)
{
    PyObject *pFunc, *pArgs, *pValue;

    pFunc = PyObject_GetAttrString(pModule, "make_list");

    pValue = PyObject_CallObject(pFunc, NULL);

    printf("Result of call: \n");
    for (int i = 0; i < PyList_Size(pValue); ++i)
    {
        printf("\t%dst item: %s \n", i, PyUnicode_AsUTF8(PyList_GetItem(pValue, i)));
    }
    Py_DECREF(pValue);
    Py_DECREF(pFunc);
}

int main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    initialize(argv[1], &pModule);

    make_string(pModule, "Hello World!");
    make_int(pModule, 99);
    make_list(pModule);

    finalize(pModule);

    return 0;
}

여기서 __init__.py 파일은 아무런 내용을 기입하지 않아도 상관없으므로, 실행을 위해서 단순히 파일만 만들어 놓자.

빌드 및 실행

: 빌드는 다음과 같은 명령어로 빌드할 수 있다.

jay@ ~/Desktop/tutorial/embedded-python $ gcc -o ./bin/main -I/usr/local/include/python3.7m ./src/main.cpp -L/usr/local/lib -lpython3.7m -lpthread -ldl -lutil -lm -Xlinker -export-dynamic

빌드 후 산출물을 가지고 실행하면 아래의 결과를 얻을 수 있을 것이다.


jay@ ~/Desktop/tutorial/embedded-python $ PYTHONPATH=. ./bin/main scripts.my_module

Result of call: my_string: Hello World! 

Result of call: 100 

Result of call: 

    0st item: 1 

    1st item: 2 

    2st item: 3

실행 중 발생되는 오류

  1. No module named pyfunction

ImportError: No module named pyfunction
  • $ PYTHONPATH=. ./main blah blah 을 통해 모듈의 위치를 찾는 경로에 현재 위치를 추가해 준다.
  1. reference undefined to "Py_Initialize", undefined referecne to 'log'

reference undefined to "Py_Initialize"

reference undefined to "PyRun_SimpleStringFlags"

reference undefined to "Py_Finalize"

collect2: error: ld returned 1 exit status





/Python-3.7.0/Objects/longobject.c:2287: undefined reference to `log'

/Python-3.7.0/Objects/floatobject.c:788: undefined reference to `pow'

/Python-3.7.0/Objects/floatobject.c:753: undefined reference to `floor'

/Python-3.7.0/Objects/floatobject.c:764: undefined reference to `fmod'

collect2: error: ld returned 1 exit status


  • $ python3-config --ldflags 수행 후 나타나는 결과를 gcc 옵션에 넣어준다.

  • 위의 해결방법으로 되지 않는다면, $ pkg-config --cflags --libs python3 수행 후 나타나는 결과를 gcc 옵션에 넣어준다. 본인은 위 방법으로 해결되었음.

  1. Python.h: No such file or directory

fatal error: Python.h: No such file or directory

compilation terminated.
  • $ sudo apt install libpython3-dev를 통해 Python 개발 관련 헤더파일을 설치해 준다.
  1. No module named 'BlahBlah'

ModuleNotFoundError: No module named 'BlahBlah'
  • module의 이름을 지정할 때 경로와 함께 지정해 준다.

  • 만일 ./module/my_module.py의 경로라면, module.my_module을 LOAD 하여야 한다.

Python2 에서 Python3로 변경시 유의해야 할 점

아래의 참고자료 중 Python2 to Python3 Porting Guide 를 보면 자세한 설명과 그 이유를 알 수 있으니 해당 문서를 참고하길 바랍니다.

: 추후 업데이트 예정

 

 

참고자료

[Ofiicial] Embedding Python Application

Python/C API Reference Manual

Python/C API Unicode Objects and Codecs

Python/C API List Objects

Python2 to Python3 Porting Guide

NLTK install guide

stackoverflow - No module name

stackoverflow - Python.h: No such file or directory

+ Recent posts