본문으로 바로가기



Make your own R package! - 9. C 함수 R 패키지에 넣기 native routines 등록에 대하여

category R Programming/Rpackage 2017.08.06 14:59
Make your own R package! - 9. C언어와 연결된 함수 R 패키지에 넣기

이제까지 우리는 R와 C를 연결하는 방법에 대하여 알아보았다. 오늘은 이렇게 작성한 함수를 우리가 만든 패키지에 넣는 방법에 대하여 알아보도록 하자.


사용할 파일 준비하기

R 파일 준비하기

우리가 패키지에 넣을 함수는 파레토 분포의 확률밀도함수를 계산하는 dpareto 함수이다. 우리가 정의할 dpareto 함수는 R 사용자로부터 입력값을 넘겨 받아 .C 함수를 사용하여 전달받은 입력값들을 C로 짜여진 함수로 넘겨주게 된다. 필자의 경우 paretodens 라는 C함수로 넘겨줄 예정이다. 이렇게 R과 C사이의 교량 역할을 하는 함수를 보통 wrapper 함수라고 하는데, 이유는 뜯어보면 C에서 작동하는 함수이지만 R코드로 이것을 ‘감싸고’ 있는 형태이기 때문이다. 아래의 코드는 C로 작성된 paretodens 함수를 사용하는 dpareto라는 이름의 R wrapper 함수이다.

dpareto <- function(x, alpha, x_m, log.p = FALSE){
    if (!is.logical(log.p) || length(log.p) != 1){
        stop("bad input for argument 'log'")
    }

    input_len <- c(length(x), length(alpha), length(x_m))
    max_len <- max(input_len)

    # Input vectorizing
    density <- vector(mode="numeric", length = max_len)

    # Call .C
    result <- .C("paretodens",
                 as.double(x),
                 as.double(alpha),
                 as.double(x_m),
                 as.integer(input_len),
                 as.integer(max_len),
                 as.integer(log.p),
                 value = as.double(density),
                 PACKAGE = "r4issactoast")$value
    if (any(is.nan(result))) {
        warning("\n All parameters should be positive.\n NaN is generated.")
    }
    result
}

PACKAGE = "r4issactoast"부분을 명시해 주는 이유는 사용자가 패키지를 설치하였을 경우 다른 패키지에서 사용하는 C코드 함수 중에 paretodens라는 이름의 함수가 있을 수 도 있기 때문에 충돌 방지를 위하여 써줘야 한다. 위와 같은 코드를 담고 있는 R파일의 이름을 pareto.R이라고 하였을때, 이전 R 패키지를 만들때와 같이 패키지안의 R폴더에 넣어준다.

pareto.R

C 파일 준비하기

이제는 위에서 언급했던 paretodens 함수를 정의해야 할 차례이다. 제일 먼저 C언어로 짜여진 paretodens 함수가 들어갈 paretoRoutines.c 파일을 준비하자. 먼저 R 패키지에 들어갈 C 코드는 R.h 헤더파일을 포함하고 있어야한다. 또한 Rmath.h를 같이 포함시켜주면 r에서 정의되어있는 분포함수와 난수 생성함수들을 C코드에서 사용할 수 있다. 필자의 경우 paretoRoutines.c라는 이름의 아래와 같은 파일을 준비하였다. 이 코드와 관련된 설명은 이전 포스팅을 참조하도록 하자.

#include <R.h>

void paretodens(double *x, double *alpha, double *x_m, 
                 int *input_len, int *max_len, int *is_log,
                 double *density){
  
  // Intialization
  int i, n, x_len, alpha_len, x_m_len, IsLog;
  
  // Prepare recycling
  n = *max_len;
  IsLog = *is_log;
  x_len = input_len[0];
  alpha_len = input_len[1];
  x_m_len = input_len[2];
  
  // For loop in C
  for (i = 0; i < n; i++){
    density[i] = R_NegInf;
  }
  
  for (i = 0; i < n; i++){
    if (alpha[i % alpha_len] <= 0.0 || x_m[i % x_m_len] <= 0.0) {
      density[i] = R_NaN;
    } else if(x[i % x_len] >= x_m[i % x_m_len]){
      density[i] = log(alpha[i % alpha_len]) + 
        alpha[i % alpha_len] * log(x_m[i % x_m_len]) -
        (alpha[i % alpha_len] + 1.0) * log(x[i % x_len]);
    }
  }
  
  if (!IsLog) {
    for ( i = 0; i < n; i++){
      density[i] = exp( density[i] );
    }
  }
}

위의 코드를 담고 있는 paretoRoutines.c 파일을 우리가 만든 패키지 폴더에 src라는 이름의 폴더를 만든 뒤 짚어넣어준다. 그리고 github를 사용하는 독자들의 경우는 .gitignore 파일을 하나 생성해서 다음과 같이 써주도록 하자.

*.o
*.so
*.dll

위의 설정은 우리가 github에 파일을 올릴때 위에서 언급된 확장자들이 같이 연동되어 올라가는 것을 방지한다.

paretoRoutines.c

roxygen 태그를 이용한 NAMESPACE 설정 및 설명서 페이지 작성

위의 과정을 잘 따라왔다면, 아래의 그림과 같이 폴더에는 pareto.R 파일이 새로만든 src 폴더에는 paretoRoutines.c 파일이 위치하고 있어야한다.

<package 상위폴더>
  NAMESPACE
  DESCRIPTION
  README.md
  ...
  <R>
    pareto.R
  <man>
  <src>
    paretoRoutines.c

이제 roxygen 태그를 이용하여 pareto.R파일 안에 정의한 dpareto 함수 위에 다음과 같은 태그를 넣어주도록 하자. roxygen 태그에 관한 자세한 설명은 이전 포스팅을 참고하자.

#' The probability density function of the pareto distribution.
#'
#' dpareto function evaluates the pdf of pareto dist. at given x with parameters.
#'
#' @param x the point where the pdf to be evaluated
#' @param alpha the shape parameter of pareto dist.
#' @param x_m the scale parameter of pareto dist.
#' @param log.p use log.p = TRUE when you want
#'  to get a result in a log scale
#' @return the pdf value of the pareto dist.
#' @examples
#'   dpareto(1:5, 2, 3)
#'   dpareto(1:5, 2, -3:3)
#'   dpareto(1:5, 2:5, 3)
#' @useDynLib r4issactoast paretodens
#' @export

위의 @useDynLib <packageName> <fcnName> 태그를 사용하면 roxygen2에서 자동으로 NAMESPACE 파일에 등록을 해준다. 위의 태그를 입력한 후 콘솔창에 다음의 명령어 입력해보도록 하자.(혹은 Ctrl + Shift + D 키를 눌러도 된다.)

devtools::document()

위의 명령어는 다음의 기능을 수행하게 된다.

  1. man 폴더 안에 dpareto.Rd 라는 설명서 파일 생성
  2. NAMESPACE 파일 안에 export(dpareto), useDynLib(r4issactoast,paretodens) 라는 문구 추가
  3. src 폴더 안의 paretoRoutines.c 파일이 자동 컴파일되어 so파일이 R에 전달됨.

C 함수 등록하기

위의 과정을 마친 후 패키지를 로딩한 후 dpareto 함수를 실행하면 다음과 같은 에러가 뜰 것이다.

library(r4issactoast)
dpareto(1:4, 2,1)
#> Error in .C("paretodens", as.double(x), as.double(alpha), as.double(x_m),  : 
#>   "paretodens" not available for .C() for package "r4issactoast"

이것은 패키지가 잘 로딩이 되어 우리가 작성한 dpareto 함수가 실행이 되었으나, paretodens 함수가 어디에 있는지 못찾는 것이다. 이전 포스팅에서 우리가 C코드로 함수를 짜고난 뒤 컴파일을 통하여 .so 혹은 .dll 파일을 생성한 것을 기억해보자. 이 파일을 생성하고 난 뒤에도 우리는 C로 짠 코드를 사용하고 싶을 때에는 dyn.load("libraryName.so")와 사용한 후에는 dyn.unload("libraryName.so") 명령어를 실행시켜줬다. 하지만 우리의 패키지는 그 기능을 가지고 있지 않으므로 에러가 나는 것이다.

사용자가 패키지를 설치하고 dpareto함수를 실행시켰을때, R에서 자동으로 패키지와 관련된 shared object가 등록되고, 함수의 사용에 따라 해당 so 파일이 로딩 되도록 해줘야한다. 이 작업을 위해서 다음과 같은 코드를 paretoRoutines.c 파일에 추가하자. (코드의 간소화를 위하여 함수의 내용은 생략하였다.)

#include <R.h>
#include <R_ext/Rdynload.h>

void paretodens(double *x, double *alpha, double *x_m, 
                 int *input_len, int *max_len, int *is_log,
                 double *density){
...
}

static const R_CMethodDef cMethods[] = {
  {"paretodens", (DL_FUNC) &paretodens, 7},
  {NULL, NULL, 0, NULL}
};

void R_init_paretoRoutines(DllInfo *info){
  R_registerRoutines(info,
                     cMethods, NULL, NULL, NULL);
  R_useDynamicSymbols(info, FALSE);
}

위의 코드에서 새로 추가된 사항은 다음과 같다.

  1. C 함수를 등록하기 위해 필요한 #include <R_ext/Rdynload.h> 헤더가 추가되었다.
  2. R_CMethodDef 함수의 경우 현재 paretoRoutines.c 안에 .C 방법과 연결되어 있는 함수가 어떤 것들이 있으며, 각 함수에 대한 정보를 R에게 알려주는 데이터를 작성하는 함수라고 생각하면 된다. .C 방법으로 “paretodens” 라는 이름의 함수를 찾을 경우 연결되어야하는 함수와 함수에 쓰이는 변수 갯수 7개를 의미한다. 이어서 뒤쪽에 붙은 NULL부분의 경우는 필자도 잘 모르겠다. 혹시 아시는 독자는 댓글 달아주시면 감사하겠다.
  3. R_init_<sofileName> 함수는 .so 파일이 R상에 로딩이 될 때, 그 파일 안에 어떠한 방법들을 사용한 함수들을 등록할 것인지에 대하여 설명해주는 함수이다. 우리의 경우 .C 방법만을 사용한 함수가 들어있으므로 cMethods를 쓰고 나머지 세 개는 NULL로 설정하였다. 만약 다른 방법들(.Call, .Fortran, .External)을 사용한다면 NULL 대신에 방법을 명시해준다.
  4. R_useDynamicSymbols의 경우는 함수를 찾을때 등록된 함수에 대하여 찾으라고 명령하는 옵션으로 알고 있는데, 충동을 방지해주는 차원으로 알고 있으며, R에서는 이 구문을 넣어줄 것을 권장하고 있다.

위와 관련한 내용은 Writing R Extensions의 Registering native routines부분을 참고 하도록 하자.

등록 과정을 마친 후 Ctrl + Shift + D 키 혹은 devtools::document() 명령어를 사용하여 패키지를 업데이트 시켜준 후 패키지를 로딩하면 우리가 등록한 함수가 잘 작동하는 것을 확인 할 수 있다.

library(r4issactoast)
dpareto(1:10, 4, 3)
##  [1] 0.000000000 0.000000000 1.333333333 0.316406250 0.103680000
##  [6] 0.041666667 0.019277682 0.009887695 0.005486968 0.003240000

Reference

[1] Writing R Extensions https://cran.r-project.org/doc/manuals/r-release/R-exts.html

[2] R News Volume 1/3, September 2001 p.20. In search of C/C++ & Fortran Routines

[3] Wickham, Hadley. R packages: organize, test, document, and share your code. " O’Reilly Media, Inc.“, 2015.


SHARE TO

신고


티스토리 툴바