DEV/C, C++

[Cmake, JNA] CMake 활용 Windows, Linux 호환 동적 라이브러리(.dll, .so) 생성 및 JNA 인터페이스 구현

Bi3a 2025. 3. 14. 12:45

반응형

개요

본 포스팅은 하기 기능의 예제를 제공합니다.

1. Visual Studio 2022를 활용한 동적 라이브러리 파일 (.so / .dll) CMake 빌드 환경 구성

2. java jna를 활용한 동적 라이브러리 파일 실행 환경 구성 

[개발 환경]
로컬 환경 : Windows x64
원격지 환경 : Linux CentOS7
IDE : Cmake (Visual Studio 2022), java JNA (IntelliJ, gradle)

사전 환경 세팅

Visual Studio 2022

설치 파일

 

Visual Studio 2022 | 무료 다운로드

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

visualstudio.microsoft.com

 

설치 방법은 아래 포스팅을 참고해주세요.

2025.04.27 - [DEV/C, C++] - [C, C++, CMake] 개발 환경을 위한 Visual Studio 2022 설치 가이드

 

[C, C++, CMake] 개발 환경을 위한 Visual Studio 2022 설치 가이드

본 메뉴얼은 Cmake, C, C++ 개발 환경 설정을 위한Windows x64와 Linux OS 호환Visual Studio 2022 설치 가이드입니다. Visual Studio 2022 설치 파일 다운 온라인 다운 링크https://visualstudio.microsoft.com/ko/vs/ Visual Studio

doinitright.tistory.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, JNA 활용 동적 라이브러리 환경 구성

새 프로젝트 만들기 클릭

 

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 파일 빌드

 

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

 

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로 변경

 

솔루션 탐색기에서 output 폴더 확인

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

 

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

 

lib 폴더가 보이지 않을 시

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

 

모두 빌드로 ninja 빌드 실행

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

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

 

dll 생성 확인

. dll 파일 생성 확인

 

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

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

  • build.ninja
  • gcc
  • g++

 

json 세팅

CMakePresets.json 에서 경로 설정

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

 

빌드 구성 변경

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

 

build file export 확인

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

 

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

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

 

so 파일 생성 확인

. 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)

프로젝트 생성 *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 클릭

 

콘솔창 업로드 후 실행

콘솔창 업로드 후 실행 확인

 

우측 상단을 통한 Main 재실행

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

 

[runnable jar 빌드 환경 구성]

 

runnable jar 빌드 환경 구성

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

 

Artifact 설정

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

 

module 설정

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

설정 후 OK 클릭

 

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

 

resources 포함 방법

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

 

ok 버튼 클릭

resources 폴더 지정 후 OK 클릭

 

빌드 간 resources 포함 여부 확인

빌드 간 포함 확인

 

설정 완료 후 output directory 확인

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

 

artifact 빌드 확인

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

 

exec.jar 확인

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

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

 

반응형