이전 글 C# AOT 라이브러리 활용에서 C++ constexprzig comptime, rust의 매크로 함수인 obfstr! 함수를 이용하여 컴파일 타임에 문자열을 난독화하는 예제를 소개했는데 이번 글에서 Native 컴파일 언어로 각각 구현하였다.

CppNative.dll , rust_native.dll , zig_native.dll (Zig는 0.16dev 버전)

graph LR
A["Native Library<br/>(Cpp, Rust, Zig)"]
-->B["Unmanaged C#(AOT)<br/>(ConsoleAppNative.dll)"]
B-->C["Manged C#<br/>(ConsoleApp.exe)"]
개발 환경
  • Cpp : Clion + 툴체인(Visusql Studio 2022)
  • Rust : RustRover + rust 1.94.1
  • Zig : VSCode + zig 0.16.0-dev.3133+5ec8e45f3
  • C# : Rider + .net 10

전체 예제는 Github에서 볼 수 있다.

Cpp : CMakeList.txt
cmake_minimum_required(VERSION 4.2)  
project(CppNative)  
set(CMAKE_CXX_STANDARD 20)  
  
add_compile_options(/utf-8)  
# 정적 컴파일  
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")  
# 동적 컴파일 : dumpbin /dependents CppNative.dll 확인  
# set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")  
add_library(CppNative SHARED  
        library.cpp  
)  
set_target_properties(CppNative PROPERTIES OUTPUT_NAME "CppNative" PREFIX "")  
  
# 동적 컴파일 시 사용  
#if (CMAKE_BUILD_TYPE STREQUAL "Debug")  
#    set(CMAKE_INSTALL_DEBUG_LIBRARIES ON)  
#endif ()  
#include(InstallRequiredSystemLibraries)  
set(DEPLOY_DIR "${CMAKE_SOURCE_DIR}/deploy/$<CONFIG>")  
add_custom_command(TARGET CppNative POST_BUILD  
        COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_DIR}"  
        COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:CppNative> "${DEPLOY_DIR}"  
        # 동적 컴파일 시 사용  
        #COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} "${DEPLOY_DIR}"  
        COMMENT "Deploying CppNative.dll and system dependencies to ${DEPLOY_DIR}"  
)
Cpp : library.h
#pragma once  
#include <string>  
#include <array>  
  
template<size_t N>  
class ObfuscatedString {  
public:  
    template<size_t... Is>  
    constexpr ObfuscatedString(const char *str, char key, std::index_sequence<Is...>)  
        : m_key(key), m_data{static_cast<char>(str[Is] ^ key)...} {  
    }  
  
    [[nodiscard]] std::string decrypt() const {  
        std::string result;  
        result.reserve(N);  
        for (size_t i = 0; i < N; ++i) {  
            result += static_cast<char>(m_data[i] ^ m_key);  
        }  
        return result;  
    }  
  
private:  
    char m_key;  
    std::array<char, N> m_data;  
};  
  
#define OBFUSCATE(str) ([]() { \  
    constexpr size_t _len = sizeof(str); \  
    return ObfuscatedString<_len>(str, 0x5A, std::make_index_sequence<_len>{}); \  
}())  
  
#if defined(_WIN32)  
#define MY_API __declspec(dllexport)  
#else  
#define MY_API __attribute__((visibility("default")))  
#endif  
  
extern "C" {  
MY_API int GetSecretData(char *buffer, int obfuscatedKey);  
}
Cpp : library.cpp
#include "library.h"  
#include <iostream>  
  
#define FIXED_BUFFER_SIZE 1024  
#define SALT 0x1234  
#define VALID_KEY 0x7777  
  
int GetSecretData(char *buffer, const int obfuscatedKey) {  
    if ((obfuscatedKey ^ SALT) != VALID_KEY) {  
        return -1;  
    }  
  
    static ObfuscatedString<sizeof ("cpp 보안 문자열")> secret = OBFUSCATE("cpp 보안 문자열");  
    const std::string decrypted = secret.decrypt();  
  
    if (buffer != nullptr) {  
        const size_t len = (decrypted.length() < FIXED_BUFFER_SIZE) ? decrypted.length() : FIXED_BUFFER_SIZE - 1;  
        memcpy(buffer, decrypted.c_str(), len);  
        buffer[len] = '\0';  
        return static_cast<int>(len);  
    }  
    return 0;  
}
Rust : Cargo.toml
[package]  
name = "rust_native"  
version = "0.1.0"  
edition = "2024"  
  
[lib]  
crate-type = ["cdylib"]  
  
[dependencies]  
obfstr = "0.4.4"  
  
[profile.release]  
opt-level = "z"   # 크기 최적화 (s 또는 z)
lto = true        # 전체 프로그램 최적화  
codegen-units = 1 # 최적화 품질 향상  
panic = "abort"   # 예외 발생 시 즉시 종료 (FFI 안전성)
Rust : config.toml
[target.x86_64-pc-windows-msvc]  
rustflags = ["-C", "target-feature=+crt-static"]  
  
[target.i686-pc-windows-msvc]  
rustflags = ["-C", "target-feature=+crt-static"]
Rust : lib.rs
use obfstr::obfbytes;  
use std::ptr;  
  
const SALT: i32 = 0x1234;  
const VALID_KEY: i32 = 0x7777;  
const MAX_INTERNAL_BUFFER: usize = 1024;  
  
#[unsafe(no_mangle)]  
pub extern "C" fn get_secret_data(buffer: *mut u8, buffer_len: i32, obfuscated_key: i32) -> i32 {  
    if buffer.is_null() || buffer_len <= 0 {  
        return 0;  
    }  
  
    if (obfuscated_key ^ SALT) != VALID_KEY {  
        return -1;  
    }  
  
    let bytes: &[u8; 21] = obfbytes!("rust 보안 문자열".as_bytes());  
    let safe_limit: usize = (buffer_len as usize).min(MAX_INTERNAL_BUFFER);  
    let copy_len: usize = bytes.len().min(safe_limit - 1);  
  
    unsafe {  
        ptr::copy_nonoverlapping(bytes.as_ptr(), buffer, copy_len);  
        ptr::write(buffer.add(copy_len), 0);  
    }  
  
    copy_len as i32  
}  
  
#[cfg(test)]  
mod tests {  
    #[test]  
    fn check_string_len() {  
        let s: &str = "rust 보안 문자열";  
        println!("바이트 길이: {}", s.len());  
        //assert_eq!(s.len(), 21);  
    }  
}  
  
// cargo test -- --nocapture  
// cargo build --release
Zig : build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const lib = b.addLibrary(.{
        .name = "zig_native",
        .linkage = .dynamic,
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    b.installArtifact(lib);
}

// zig build -Doptimize=ReleaseSmall
Zig : main.zig
const std = @import("std");

const SALT: i32 = 0x1234;
const VALID_KEY: i32 = 0x7777;
const XOR_KEY: u8 = 0x5A;
const MAX_INTERNAL_BUFFER: usize = 1024;

fn obfuscate(comptime str: []const u8) [str.len]u8 {
    var res: [str.len]u8 = undefined;
    for (str, 0..) |c, i| {
        res[i] = c ^ XOR_KEY;
    }
    return res;
}

const secret_data = obfuscate("zig 보안 문자열");

export fn get_secret_data(buffer: [*]u8, buffer_len: i32, obfuscated_key: i32) callconv(.c) i32 {
    if (buffer_len <= 0) {
        return 0;
    }

    if ((obfuscated_key ^ SALT) != VALID_KEY) {
        return -1;
    }

    const safe_limit = @as(usize, @intCast(buffer_len));

    const internal_limit =
        if (safe_limit < MAX_INTERNAL_BUFFER)
            safe_limit
        else
            MAX_INTERNAL_BUFFER;

    const copy_len =
        if (secret_data.len < internal_limit)
            secret_data.len
        else
            internal_limit - 1;

    var i: usize = 0;

    while (i < copy_len) : (i += 1) {
        buffer[i] = secret_data[i] ^ XOR_KEY;
    }

    buffer[copy_len] = 0;
    return @as(i32, @intCast(copy_len));
}
C# : ConsoleApp.csproj
<Project Sdk="Microsoft.NET.Sdk">  
    <PropertyGroup>  
        <OutputType>Exe</OutputType>  
        <TargetFramework>net10.0</TargetFramework>  
        <PublishSingleFile>true</PublishSingleFile>  
        <SelfContained>true</SelfContained>  
        <RuntimeIdentifier>win-x64</RuntimeIdentifier>  
        <ImplicitUsings>disable</ImplicitUsings>  
        <Nullable>enable</Nullable>  
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>  
        <ApplicationIcon>main.ico</ApplicationIcon>  
        <Version>1.0.0</Version>  
        <Copyright>Copyright © 2026 devsight.kr</Copyright>  
        <!-- 복사 충돌 시 -->  
        <!-- <ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles> -->    </PropertyGroup>  
  
    <!-- managed 라이브러리 PublishSingleFile 빌드할 때 EXE에 포함하지 않음 -->  
    <ItemGroup>  
        <ProjectReference Include="..\ConsoleAppLib\ConsoleAppLib.csproj">  
            <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
        </ProjectReference>  
    </ItemGroup>  
  
    <!-- AOT 라이브러리 빌드 -->  
    <ItemGroup>  
        <ProjectReference Include="..\ConsoleAppNative\ConsoleAppNative.csproj" ReferenceOutputAssembly="false">  
            <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
        </ProjectReference>  
    </ItemGroup>  
  
    <PropertyGroup>  
        <AotDllSourcePath>..\ConsoleAppNative\bin\$(Configuration)\$(TargetFramework)\win-x64\publish\ConsoleAppNative.dll</AotDllSourcePath>  
    </PropertyGroup>  
  
    <Target Name="PublishNativeAotLibrary" BeforeTargets="BeforeBuild;ComputeFilesToPublish">  
        <Message Importance="High" Text="[AOT 빌드 시작] ConsoleAppNative 프로젝트를 네이티브 DLL로 게시(Publish) 중..."/>  
        <Exec Command="dotnet publish &quot;..\ConsoleAppNative\ConsoleAppNative.csproj&quot; -c $(Configuration) -r win-x64"              StdOutEncoding="utf-8" StdErrEncoding="utf-8"/>  
    </Target>  
  
    <Target Name="CopyAotDllForBuild" AfterTargets="PublishNativeAotLibrary" Condition="'$(PublishProtocol)' == ''">  
        <Message Importance="High" Text="[빌드 모드] 생성된 네이티브 DLL을 출력 폴더로 복사합니다."/>  
        <Copy SourceFiles="$(AotDllSourcePath)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true"/>  
    </Target>  
  
    <Target Name="ExcludeAotDllFromSingleFile" AfterTargets="PublishNativeAotLibrary" BeforeTargets="ComputeFilesToPublish">  
        <Message Importance="High" Text="[배포 모드] 네이티브 DLL을 단일 파일에서 제외하고 외부 배포 목록에 추가합니다."/>  
        <ItemGroup>  
            <ResolvedFileToPublish Include="$(AotDllSourcePath)">  
                <RelativePath>ConsoleAppNative.dll</RelativePath>  
                <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>  
                <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
            </ResolvedFileToPublish>  
        </ItemGroup>  
    </Target>  
  
    <ItemGroup>  
        <None Remove="main.ico"/>  
        <Resource Include="main.ico"/>  
    </ItemGroup>  
  
    <!-- 메인(ConsoleApp)프로젝트에 이미 포함되어 있을 때 -->  
    <ItemGroup>  
         <None Update="CppNative.dll">  
             <CopyToOutputDirectory>Always</CopyToOutputDirectory>  
             <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
         </None>  
        <None Update="rust_native.dll">  
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>  
            <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
        </None>  
        <None Update="zig_native.dll">  
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>  
            <ExcludeFromSingleFile>true</ExcludeFromSingleFile>  
        </None>  
         <!--  
        <None Update="Database\test.db">
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        </None>
        -->
    </ItemGroup>  
</Project>
# 싱글 파일 배포
dotnet publish -c Release : csproj에서 설정한 경우
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true  

C#에 사용된 전체 예제는 Github에서 확인할 수 있다. 폴더명은 csProject / ConsoleProject / 3개 프로젝트.

참고로 Zig 컴파일러는 아직 정식 버전으로 확정하기 전이므로 버전별로 호환에 문제가 발생할 수 있다.