DEV/C, C++

[동적 라이브러리] CMAKE, JNA 활용 라이브러리 생성 및 실행환경 구현

Bi3a 2025. 3. 14. 12:45

728x90

개요

본 포스팅은 하기 기능의 예제를 제공합니다.
1. Visual Studio를 활용한 동적 라이브러리 파일 (.so, .dll) CMake 빌드 환경 구성
2. IntelliJ를 활용한 동적 라이브러리 파일 실행 환경 구성 

사전 환경 세팅

Visual Studio 2022

설치파일

 

Visual Studio 2022 | 무료 다운로드

Visual Studio에서 코드 완성, 디버깅, 테스트, Git 관리, 클라우드 배포를 사용하여 코드를 작성합니다. 지금 무료로 커뮤니티를 다운로드하세요.

visualstudio.microsoft.com

 

MinGW

설치파일(x64)

 

Releases · niXman/mingw-builds-binaries

MinGW-W64 compiler binaries. Contribute to niXman/mingw-builds-binaries development by creating an account on GitHub.

github.com

 

Visual Studio 구성 (Cmake)

프로젝트 생성

새 프로젝트 만들기 클릭

 

CMake 프로젝트 클릭

  • 프로젝트 이름 : MakeDll
  • 프로젝트 위치 : 개별 지정
  • '솔루션 및 프로젝트를 같은 디렉터리에 배치' 체크
    • 체크 해제 시 : /{프로젝트명}/{프로젝트명} 구조로 디렉토리가 생성됨

 

참고) 예제의 프로젝트 구조

MakeDll // Root Dir
ㄴ lib // dll export Dir
   ㄴ build.ninja // makefile (ninja로 라이브러리 파일 빌드)
   ㄴ MyMath.dll // Windows 빌드 시
   ㄴ MyMath.so // Linux 빌드 시
ㄴ CMakeLists.txt // 빌드 간 소스 파일 / 출력명 설정
ㄴ CMakePresets.json // 빌드 간 출력 디렉토리 설정
ㄴ MyMath.cpp // 소스 파일 
ㄴ MyMath.h // 헤더 파일

(본 예제는 java 프로젝트 아래에 본 MakeDll 디렉토리를 위치시킴)

 

최초 프로젝트 생성 시 만들어지는 기본 파일(총 4개)

 

참고) CMake 프로젝트 닫은 후 다시 여는 방법

파일 > 열기 > CMakeCMakeLists.txt 파일 지정 후 열기 클릭

소스 파일 구성

CmakeLists.txt

# 소스 파일
set(SOURCE_FILES MyMath.cpp)

# 동적 라이브러리 생성
add_library(MakeDll SHARED ${SOURCE_FILES})

# lib 접두사 제거 (Linux 환경 .so 파일 default 접두사)
set(CMAKE_SHARED_LIBRARY_PREFIX "")

# 출력 파일 이름 지정
set_target_properties(MakeDll PROPERTIES OUTPUT_NAME "MyMath")

# 라이브러리 출력 디렉토리 설정 (선택 사항)
## CMAKE_BINARY_DIR은 CMakePresets.json에서 설정 변경 가능
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")

CMakePresets.json: 빌드 간 출력 경로, 빌드 툴, 컴파일러 지정

{
  "version": 3,
  "configurePresets": [
    {
      "name": "windows-base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/lib",
      "cacheVariables": {
        "CMAKE_C_COMPILER": "cl.exe",
        "CMAKE_CXX_COMPILER": "cl.exe"
      }
    },
    {
      "name": "x64-debug",
      "displayName": "x64 Debug",
      "inherits": "windows-base",
      "architecture": {
        "value": "x64",
        "strategy": "external"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "x64-release",
      "displayName": "x64 Release",
      "inherits": "windows-base",
      "architecture": {
        "value": "x64",
        "strategy": "external"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "linux-debug",
      "displayName": "Linux Debug",
      "generator": "Ninja",
      "binaryDir": "/home/mrisk/sjlee/MakeDll/lib", // Custom, /MakeDll/lib 은 고정
      "cacheVariables": {
        "CMAKE_C_COMPILER": "/usr/bin/gcc",
        "CMAKE_CXX_COMPILER": "/usr/bin/g++",
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ]
}
  • 윈도우의 경우 프로젝트 폴더 예하에 lib이 생성됨
  • 리눅스(원격지)의 경우 /MakeDll/lib 상위의 경로는 별도 설정

MyMath.cpp

#include "MyMath.h"

double Sum(double a, double b) {
    return a + b;
}

double Sub(double a, double b) {
    return a - b;
}

double Mul(double a, double b) {
    return a * b;
}

double Div(double a, double b) {
    if (b != 0) {
        return a / b;
    }
    else {
        // Zero division error
        return 0; 
    }
}

MyMath.h

#pragma once

#ifdef _WIN32  // For Windows
#ifdef BUILD_MY_MATH_LIBRARY
#define MY_MATH_API __declspec(dllexport)  // DLL 출력
#else
#define MY_MATH_API __declspec(dllimport)  // DLL 입력
#endif
#elif __linux__  // For Linux
#define MY_MATH_API __attribute__((visibility("default")))  // .so Export
#else
#error "Unsupported platform"
#endif

extern "C" {
    MY_MATH_API double Sum(double a, double b);
    MY_MATH_API double Sub(double a, double b);
    MY_MATH_API double Mul(double a, double b);
    MY_MATH_API double Div(double a, double b);
}

 

리눅스 원격지 연결 설정

도구 > 옵션

 

추가 버튼 클릭

 

원격지 정보 입력 후 연결

 

CMake로 dll, so 파일 빌드

[로컬, 원격지 공통 빌드 방법]

소스 코드에서 Ctrl + S : 루트 폴더 예하의 /lib 폴더가 만들어지며, 이하의 build.ninja 빌드 파일을 만든다.

이후 build.ninja가 위치한 디렉토리에서 cmd나 bash 등 cli에서 ninja 명령어를 입력 시 수동으로 .so, .dll 파일이 빌드됨

 

  • 빌드 > 모두 빌드 : 루트 폴더 예하의 /lib 폴더 및 .so, .dll 파일 빌드까지 실행
  • 빌드 > 모두 다시 빌드 : make clean → make → makefile로 빌드 캐시 수행 후 재빌드
  • 빌드 > 모두 정리 : make clean 실행

 

[로컬 빌드 방법]

빌드 구성을 로컬 컴퓨터, x64 Debug로 변경

 

최초 변경 시 /lib 폴더가 생성됨

 

참고) lib 폴더가 보이지 않을 시

모든 파일 보기 설정을 활성화 시 루트 디렉토리 예하의 모든 파일 열람이 가능

 

빌드 > 모두 빌드 클릭 (Ctrl + Shift + B)

(혹은 해당 lib 경로에서 cmd > ninja 명령어 수행)

 

.dll 파일 생성 확인

 

[원격지 빌드 방법 (Linux)]

참고) 원격지 빌드 구성간 설치 필요 라이브러리

  • build.ninja
  • gcc
  • g++

 

CMakePresets.json 에서 경로 설정

/MakeDll/lib을 제외한 상위 경로를 설정 (so 파일 위치를 잡는다)

 

빌드 구성을 원격지 IP, Linux Debug로 변경

 

build file (build.ninja가 export 되었음을 확인)

 

빌드 > 모두 빌드 클릭 (Ctrl + Shift + B)

(혹은 해당 lib 경로에서 bash > ninja 명령어 수행)

 

.so 파일 생성 확인

 

JNA java application 구성 (실행)

(본 예제는 java 프로젝트 아래에 상기 작업한 CMake 프로젝트인 MakeDll 디렉토리를 위치시킴)

exec // Root Dir
ㄴ gradle
ㄴ bin 
ㄴ out // 빌드된 jar 파일이 위치할 디렉토리
ㄴ MakeDll // CMake 프로젝트 (상기 작업 폴더)
ㄴ src
   ㄴ main
      ㄴ java.org.jna
         ㄴ Main.java 
         ㄴ LibraryRunner.java // 기본 시스템 기능 정의
         ㄴ NativeLibraryLoader.java // 함수 리스트 출력, 실행
         ㄴ Config.java // properties 객체
      ㄴ resources
         ㄴ application.properties // .so, .dll 파일경로 설정         
ㄴ build.gradle // 의존성 주입 (기존 pom.xml 기능)
ㄴ settings.gradle
ㄴ gradlew
ㄴ gradlew.bat

 

새 프로젝트 생성(IntelliJ)

  • Name : exec
  • Location : 지정 (단, java 프로젝트 루트 디렉토리 아래에 MakeDll 디렉토리가 포함되게 설정)
  • Language : Java
  • Build System : Gradle
  • JDK : 1.8
  • Gradle DSL : Groovy
  • Advanced Settings > GroupId : org.jna

 

설정 후 Create 클릭

 

생성 확인

 

소스 파일 구성

 

/build.gradle

plugins {
    id 'java'
}

group 'org.jna'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'net.java.dev.jna:jna:5.11.0'
    implementation 'org.slf4j:slf4j-api:1.7.32'
    implementation 'org.slf4j:slf4j-simple:1.7.32'
    compileOnly 'org.projectlombok:lombok:1.18.22'
    annotationProcessor 'org.projectlombok:lombok:1.18.22'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
    useJUnitPlatform()
}

/src/main/java/org/jna/Config.java

package org.jna;

import lombok.Getter;
import lombok.AllArgsConstructor;
import java.util.Properties;

@Getter
@AllArgsConstructor
public class Config {
    private String execFileDir;
    private String execFileName;

    public static Config fromProperties(Properties properties) {
        return new Config(
                properties.getProperty("execFileDir"),
                properties.getProperty("execFileName")
        );
    }
}

/src/main/java/org/jna/Main.java

package org.jna;

import java.io.InputStream;
import java.util.Properties;

public class Main {
    public static void main(String[] args) {
        Config config = loadConfig();
        LibraryRunner.runNativeLibrary(config);
    }

    private static Config loadConfig() {
        Properties properties = new Properties();
        try (InputStream input = Main.class.getClassLoader().getResourceAsStream("application.properties")) {
            if (input == null) {
                throw new RuntimeException("Unable to find application.properties");
            }
            properties.load(input);
        } catch (Exception e) {
            throw new RuntimeException("Failed to load configuration", e);
        }
        return Config.fromProperties(properties);
    }
}

/src/main/java/org/jna/LibraryRunner.java

package org.jna;

import com.sun.jna.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Scanner;

public class LibraryRunner {
    private static final Logger logger = LoggerFactory.getLogger(LibraryRunner.class);

    public static void runNativeLibrary(Config config) {
        if (config == null) {
            logger.error("Configuration is missing.");
            return;
        }

        String osFile;
        if (Platform.isWindows()) {
            osFile = config.getExecFileName() + ".dll";
            logger.info("::::: [DYNAMIC LIBRARY RUNNER] for Windows :::::");
        } else if (Platform.isLinux()) {
            osFile = config.getExecFileName() + ".so";
            logger.info("::::: [DYNAMIC LIBRARY RUNNER] for Linux :::::");
        } else if (Platform.isMac()) {
            osFile = config.getExecFileName() + ".dylib";
        } else {
            logger.error("호환되지 않는 운영체제입니다.");
            return;
        }

        logger.info("Loading native library : {}", osFile);
        NativeLibraryLoader loader = new NativeLibraryLoader(config.getExecFileDir() + "/" + osFile);
        Scanner scanner = new Scanner(System.in);
        logger.info("Successfully loaded library : {}", osFile);
        logger.info("명령어를 입력해주세요. [-help, -list, -{functionName}]");

        while (true) {
            String input = scanner.nextLine().trim();

            if ("-exit".equals(input)) {
                logger.info("시스템을 종료합니다.");
                break;
            } else if ("-list".equals(input)) {
                logger.info("[{}] 사용 가능 함수", config.getExecFileName());
                for (String function : loader.getFunctions())
                    logger.info("{}. {}", loader.getFunctions().indexOf(function) + 1, function);
            } else if ("-help".equals(input)) {
                logger.info("사용 가능한 명령어");
                logger.info("-list : 호출 가능한 함수를 출력합니다.");
                logger.info("-exit : 시스템을 종료합니다.");
                logger.info("-{함수명} : 함수를 호출합니다.");
            } else if (input.startsWith("-") && loader.getFunctions().contains(input.replaceFirst("^-+", ""))) {
                String inputSubstrBar = input.replaceFirst("^-+", "");

                logger.info("[{}] 입력값 1", inputSubstrBar);
                String param1 = scanner.nextLine().trim();
                logger.info("[{}] 입력값 2", inputSubstrBar);
                String param2 = scanner.nextLine().trim();

                logger.info("[{}] 계산 결과 : {} ", inputSubstrBar, loader.callFunction(inputSubstrBar, param1, param2));
            } else {
                logger.warn("알 수 없는 명령어입니다. -help를 입력해 보세요.");
            }
        }
    }
}

/src/main/java/org/jna/NativeLibraryLoader.java

package org.jna;

import com.sun.jna.NativeLibrary;
import com.sun.jna.Platform;
import lombok.Getter;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Getter
public class NativeLibraryLoader {
    private final String libraryPath;

    private final List<String> functions;

    public NativeLibraryLoader(String libraryPath) {
        this.libraryPath = libraryPath;
        this.functions = new ArrayList<>();
        loadFunctionList();
    }

    private void loadFunctionList() {
        try {
            ProcessBuilder processBuilder;

            if (Platform.isWindows()) {
                // Windows : 함수 목록 파싱 (dumpbin / EXPORTS)
                processBuilder = new ProcessBuilder("cmd.exe", "/s", "/c", "dumpbin /EXPORTS \"" + libraryPath + "\"");
            } else if (Platform.isLinux()) {
                // Linux : 함수 목록 파싱 (nm -D)
                processBuilder = new ProcessBuilder("nm", "-D", libraryPath);
            } else {
                return;
            }

            Process process = processBuilder.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

            String line = "";
            while ((line = reader.readLine()) != null) {
                if (Platform.isWindows()) {
                    String functionNamePattern = ".*\\((\\w+)\\).*"; // 괄호 안의 함수명 추출
                    Pattern pattern = Pattern.compile(functionNamePattern);
                    Matcher matcher = pattern.matcher(line);
                    // "R", "C" 등 네이티브 함수 제외
                    if (matcher.matches()) {
                        if (!matcher.group(1).equals("R") && !matcher.group(1).equals("C")) {
                            functions.add(matcher.group(1));
                        }
                    }
                } else {
                    // Linux : 함수 목록 파싱 (nm
                    if (line.contains(" T ")) {
                        String[] parts = line.trim().split("\\s+");
                        // "__init", "__fini" 등 네이티브 함수 제외
                        if (!parts[parts.length -1].startsWith("_")) {
                            functions.add(parts[parts.length - 1]); // 마지막 컬럼이 함수명
                        }
                    }
                }
            }

            reader.close();
            process.waitFor();
        } catch (Exception e) {
            throw new RuntimeException("Failed to load functions from native library: " + e.getMessage(), e);
        }
    }

    // 함수명을 입력하면 해당 함수를 실행하는 메서드 (JNA를 활용)
    public String callFunction(String functionName, String param1, String param2) {
        try {
            NativeLibrary library = NativeLibrary.getInstance(libraryPath);
            return library.getFunction(functionName).invoke(
                    double.class, new Object[]{Double.parseDouble(param1), Double.parseDouble(param2)}
            ).toString();
        } catch (Exception e) {
            throw new RuntimeException("Failed to call function: " + functionName, e);
        }
    }
}

 

빌드 / 실행 환경 구성

[소스로 컴파일, 실행]

 

Main 소스 코드 진입 후 Line 6의 화살표 클릭 > Run Main.main 클릭

 

콘솔창 업로드 후 실행 확인

 

최초 실행 후에는 우측 상단을 통해 재실행 가능

 

[runnable jar 빌드 환경 구성]

ctrl + alt + shift + s 또는 File > Project Structure 진입

 

좌측부터 Artifacts > + 버튼 > Jar > From modules with dependencies 클릭

 

  • Module : <All Modules>
  • Main Class : 프로젝트 browse 후 org.jna의 Main 지정 후 OK
  • Drirectory for META-INF/MANIFEST.MF : MANIFEST.MF 생성 폴더 지정 (예제의 경우 src 예하로)

설정 후 OK 클릭

 

참고) Available Elements에 resources 디렉토리 미포함 시 조치사항

빌드 시 resources를 포함하기 위해 + 버튼 > Directory Content 클릭

 

resources 폴더 지정 후 OK 클릭

 

빌드 간 포함 확인

 

설정 완료 후 OK 클릭, 이후 빌드 완료된 jar는 위의 Output Directory에 탑재됨

 

Build > Build Artifacts > exec.jar > Build로 빌드

 

/out/artifacts/exec_jar/exec.jar 생성 확인

리눅스 환경에서 java -jar 실행 시 MakeDll과 같은 디렉토리에 jar 파일이 위치해야함 (예제 소스 기준)