FreeBSD에서 Skia C 바인딩 라이브러리 생성 방법 (rust-skia 활용)

서론: C++ ABI 불일치 문제

이전 게시물(FreeBSD에서 Skia 컴파일하는 방법)에서는 gnninja를 사용하여 Skia C++ 라이브러리(libskia.a)를 직접 컴파일하는 방법을 다루었습니다. 이 방법은 viewer와 같이 gn 빌드 시스템 내에서 통합되는 예제를 실행하는 데 유효합니다.

그러나 이 libskia.a 라이브러리를 Makefile이나 CMake를 사용하는 외부 C/C++ 프로젝트에서 링크하여 사용하려 할 때, C++ ABI (Application Binary Interface) 불일치 문제가 발생합니다.

  • 원인: Skia의 C++ 빌드 시스템(gn)은 skia_use_vulkan=true와 같은 전처리기 플래그(-D...)를 정의합니다. DisplayParams.h와 같은 Skia 헤더 파일들은 이 플래그에 따라 구조체의 메모리 레이아웃(크기)을 변경합니다.
  • 문제: Makefile로 컴파일되는 C 프로젝트는 gn이 사용한 플래그를 알지 못하므로, 라이브러리(libskia.a)와 C 프로젝트 코드 간에 동일한 구조체의 크기가 달라지는 ABI 불일치가 발생합니다. 이는 Invalid read 또는 SIGSEGV (메모리 충돌)를 유발합니다.

rust-skia를 활용한 C API 솔루션

rust-skia 프로젝트는 이 ABI 문제를 해결하는 C API 브리지(Bridge)를 제공합니다. rust-skiacargo build 프로세스는 다음 작업을 수행합니다.

  1. Skia C++ 코어(libskia.a)를 gn으로 빌드합니다.
  2. gn이 사용한 C++ 컴파일 플래그를 캡처합니다.
  3. C API 래퍼(bindings.cpp)를 2단계에서 캡처한 동일한 플래그로 컴파일하여 libskia-bindings.a를 생성합니다.

이 과정을 통해 libskia.alibskia-bindings.a는 ABI가 호환되는 C 라이브러리 세트가 됩니다.

이 글은 C 툴킷 개발을 위해 rust-skia의 빌드 시스템을 활용하여 Skia C 라이브러리(libskia.a, libskia-bindings.a)를 생성하는 방법을 설명합니다.


1. rust-skia 소스 코드 다운로드

git을 사용하여 skia 하위 모듈(submodule)을 포함한 소스 코드를 복제해야 합니다.

# m142 (0.90.0) 버전을 기준으로 복제합니다.
git clone --recursive --branch 0.90.0 https://github.com/rust-skia/rust-skia.git rust-skia-0.90.0
cd rust-skia-0.90.0

2. Skia C++ 소스 코드 패치

rust-skia의 빌드 스크립트가 컴파일할 대상인 skia-bindings/skia 하위 모듈에 FreeBSD 지원 패치를 적용해야 합니다.

  1. Skia 하위 모듈 디렉터리로 이동:

    cd skia-bindings/skia
    
  2. 패치 적용: (FreeBSD 지원 패치 파일이 ~/support-freebsd-skia-m142.diff에 있다고 가정합니다.)

    commit bd66f5163f7ed1360c3d17c80fbe4c465d4e1171
    Author: Hodong Kim <hodong@nimfsoft.art>
    Date:   Fri Nov 14 01:31:35 2025 +0900
    
        Support for FreeBSD, skia-m142
    
    diff --git a/BUILD.gn b/BUILD.gn
    index 74de313c2f..0820913e62 100644
    --- a/BUILD.gn
    +++ b/BUILD.gn
    @@ -31,7 +31,7 @@ config("skia_public") {
      if (is_component_build) {
        defines += [ "SKIA_DLL" ]
      }
    -  if (is_linux) {
    +  if (is_linux || is_freebsd) {
        defines += [ "SK_R32_SHIFT=16" ]
      }
      if (skia_enable_optimize_size) {
    @@ -589,6 +589,8 @@ if (skia_compile_modules) {
          sources += [ "src/utils/SkGetExecutablePath_mac.cpp" ]
        } else if (is_linux || is_android) {
          sources += [ "src/utils/SkGetExecutablePath_linux.cpp" ]
    +    } else if (is_freebsd) {
    +      sources += [ "src/utils/SkGetExecutablePath_freebsd.cpp" ]
        }
        if (is_win) {
          sources += skia_ports_windows_sources
    @@ -716,6 +718,8 @@ if (skia_compile_sksl_tests) {
          sources += [ "src/utils/SkGetExecutablePath_mac.cpp" ]
        } else if (is_linux || is_android) {
          sources += [ "src/utils/SkGetExecutablePath_linux.cpp" ]
    +    } else if (is_freebsd) {
    +      sources += [ "src/utils/SkGetExecutablePath_freebsd.cpp" ]
        }
        if (is_win) {
          sources += skia_ports_windows_sources
    @@ -1002,7 +1006,7 @@ optional("gpu") {
          }
        } else if (skia_use_webgl) {
          sources += [ "src/gpu/ganesh/gl/webgl/GrGLMakeNativeInterface_webgl.cpp" ]
    -    } else if (is_linux && skia_use_x11) {
    +    } else if ((is_linux || is_freebsd) && skia_use_x11) {
          sources += [
            "src/gpu/ganesh/gl/glx/GrGLMakeGLXInterface.cpp",
            "src/gpu/ganesh/gl/glx/GrGLMakeNativeInterface_glx.cpp",
    @@ -1789,7 +1793,7 @@ skia_component("skia") {
        ]
      }
        
    -  if (is_linux || is_wasm) {
    +  if (is_linux || is_freebsd || is_wasm) {
        sources += [ "src/ports/SkDebug_stdio.cpp" ]
        if (skia_use_egl) {
          libs += [ "GLESv2" ]
    @@ -1985,7 +1989,7 @@ if (((skia_enable_fontmgr_fontconfig && skia_use_freetype) ||
        
    # Targets guarded by skia_enable_tools may use //third_party freely.
    if (skia_enable_tools) {
    -  if (is_linux && target_cpu == "x64") {
    +  if ((is_linux || is_freebsd) && target_cpu == "x64") {
        skia_executable("fiddle") {
          check_includes = false
          libs = []
    @@ -2155,7 +2159,7 @@ if (skia_enable_tools) {
          if (is_android || skia_use_egl) {
            sources += [ "tools/ganesh/gl/egl/CreatePlatformGLTestContext_egl.cpp" ]
            libs += [ "EGL" ]
    -      } else if (is_linux) {
    +      } else if (is_linux || is_freebsd) {
            sources += [ "tools/ganesh/gl/glx/CreatePlatformGLTestContext_glx.cpp" ]
            libs += [
              "GLU",
    @@ -2586,7 +2590,7 @@ if (skia_enable_tools) {
        ]
      }
        
    -  if (is_linux || is_mac || skia_enable_optimize_size) {
    +  if (is_linux  || is_freebsd || is_mac || skia_enable_optimize_size) {
        if (skia_enable_skottie) {
          test_app("skottie_tool") {
            deps = [ "modules/skottie:tool" ]
    @@ -2764,7 +2768,7 @@ if (skia_enable_tools) {
        }
      }
        
    -  if (is_linux && skia_use_icu) {
    +  if ((is_linux || is_freebsd) && skia_use_icu) {
        test_app("sktexttopdf") {
          sources = [ "tools/using_skia_and_harfbuzz.cpp" ]
          deps = [
    @@ -2774,7 +2778,7 @@ if (skia_enable_tools) {
        }
      }
        
    -  if (is_linux || is_mac) {
    +  if (is_linux || is_freebsd || is_mac) {
        test_app("create_test_font") {
          sources = [ "tools/fonts/create_test_font.cpp" ]
          deps = [ ":skia" ]
    @@ -3023,7 +3027,7 @@ if (skia_enable_tools) {
            "tools/sk_app/android/surface_glue_android.h",
          ]
          libs += [ "android" ]
    -    } else if (is_linux) {
    +    } else if (is_linux || is_freebsd) {
          sources += [
            "tools/sk_app/unix/Window_unix.cpp",
            "tools/sk_app/unix/Window_unix.h",
    @@ -3246,7 +3250,7 @@ if (skia_enable_tools) {
        }
      }
        
    -  if (is_linux || is_win || is_mac) {
    +  if (is_linux || is_freebsd || is_win || is_mac) {
        test_app("editor") {
          is_shared_library = is_android
          deps = [ "modules/skplaintexteditor:editor_app" ]
    diff --git a/bench/SkSLBench.cpp b/bench/SkSLBench.cpp
    index 2af4081af3..1e7c20d262 100644
    --- a/bench/SkSLBench.cpp
    +++ b/bench/SkSLBench.cpp
    @@ -625,10 +625,25 @@ void main()
        
    #if defined(SK_BUILD_FOR_UNIX)
        
    +#ifdef __FreeBSD__
    +#include <stdlib.h>
    +#include <malloc_np.h>
    +
    +static int64_t heap_bytes_used() {
    +  size_t allocated;
    +  size_t len = sizeof (allocated);
    +
    +  if (!mallctl ("stats.allocated", &allocated, &len, NULL, 0))
    +    return allocated;
    +
    +  return -1;
    +}
    +#else
    #include <malloc.h>
    static int64_t heap_bytes_used() {
        return (int64_t)mallinfo().uordblks;
    }
    +#endif
        
    #elif defined(SK_BUILD_FOR_MAC) || defined(SK_BUILD_FOR_IOS)
        
    diff --git a/gn/BUILDCONFIG.gn b/gn/BUILDCONFIG.gn
    index b08ab14c59..70ca1d5dfd 100644
    --- a/gn/BUILDCONFIG.gn
    +++ b/gn/BUILDCONFIG.gn
    @@ -67,6 +67,7 @@ is_android = current_os == "android"
    is_ios = current_os == "ios" || current_os == "tvos"
    is_tvos = current_os == "tvos"
    is_linux = current_os == "linux"
    +is_freebsd = current_os == "freebsd"
    is_mac = current_os == "mac"
    is_wasm = current_os == "wasm"
    is_win = current_os == "win"
    diff --git a/gn/skia/BUILD.gn b/gn/skia/BUILD.gn
    index 75d8f62b7b..6b734690fe 100644
    --- a/gn/skia/BUILD.gn
    +++ b/gn/skia/BUILD.gn
    @@ -253,7 +253,7 @@ config("default") {
        }
      }
        
    -  if (is_linux) {
    +  if (is_linux || is_freebsd) {
        libs += [ "pthread" ]
      }
        
    @@ -377,7 +377,7 @@ config("default") {
          ldflags += [ "-fsanitize=$sanitizers" ]
        }
        
    -    if (is_linux) {
    +    if (is_linux || is_freebsd) {
          cflags_cc += [ "-stdlib=libc++" ]
          ldflags += [ "-stdlib=libc++" ]
        }
    @@ -770,7 +770,7 @@ config("executable") {
        ]
      } else if (is_mac) {
        ldflags = [ "-Wl,-rpath,@loader_path/." ]
    -  } else if (is_linux) {
    +  } else if (is_linux || is_freebsd) {
        ldflags = [
          "-rdynamic",
          "-Wl,-rpath,\$ORIGIN",
    diff --git a/gn/toolchain/BUILD.gn b/gn/toolchain/BUILD.gn
    index d4148ed6fa..41daf20c8e 100644
    --- a/gn/toolchain/BUILD.gn
    +++ b/gn/toolchain/BUILD.gn
    @@ -306,7 +306,7 @@ template("gcc_like_toolchain") {
            rspfile = ".rsp"
            rspfile_content = ""
            rm_py = rebase_path("../rm.py")
    -        command = "$shell python3 \"$rm_py\" \"\" && $ar rcs  @$rspfile"
    +        command = "$shell python3 \"$rm_py\" \"\" && $ar rcs  `cat $rspfile`"
          }
        
          outputs =
    diff --git a/src/utils/BUILD.bazel b/src/utils/BUILD.bazel
    index 613abca937..c612f34475 100644
    --- a/src/utils/BUILD.bazel
    +++ b/src/utils/BUILD.bazel
    @@ -153,6 +153,7 @@ skia_cc_library(
            "@platforms//os:windows": ["SkGetExecutablePath_win.cpp"],
            "@platforms//os:macos": ["SkGetExecutablePath_mac.cpp"],
            "@platforms//os:linux": ["SkGetExecutablePath_linux.cpp"],
    +        "@platforms//os:freebsd": ["SkGetExecutablePath_freebsd.cpp"],
        }),
        hdrs = ["SkGetExecutablePath.h"],
        visibility = [
    diff --git a/src/utils/SkGetExecutablePath_freebsd.cpp b/src/utils/SkGetExecutablePath_freebsd.cpp
    new file mode 100644
    index 0000000000..d199b970db
    --- /dev/null
    +++ b/src/utils/SkGetExecutablePath_freebsd.cpp
    @@ -0,0 +1,27 @@
    +/*
    + * Copyright 2022 Google Inc.
    + * Copyright (C) 2023-2025 Hodong Kim <hodong@nimfsoft.art>
    + *
    + * Use of this source code is governed by a BSD-style license that can be
    + * found in the LICENSE file.
    + */
    +
    +#include "tools/SkGetExecutablePath.h"
    +#include <sys/types.h>
    +#include <sys/sysctl.h>
    +
    +std::string SkGetExecutablePath() {
    +    int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
    +    std::string result(PATH_MAX, '\0');
    +
    +    size_t len = result.size();
    +
    +    int retval = sysctl(mib, 4, &result[0], &len, NULL, 0);
    +
    +    if (retval < 0) {
    +        result.clear();
    +    } else {
    +        result.resize((len > 0) ? (len - 1) : 0);
    +    }
    +    return result;
    +}
    diff --git a/tools/git-sync-deps b/tools/git-sync-deps
    index feaff62f29..ee1aa828a4 100755
    --- a/tools/git-sync-deps
    +++ b/tools/git-sync-deps
    @@ -96,7 +96,7 @@ def is_git_toplevel(git, directory):
        # return which breaks the comparison.
        toplevel = subprocess.check_output(
          [git, 'rev-parse', '--path-format=relative', '--show-toplevel'], cwd=directory).strip()
    -    return (os.path.normcase(os.path.realpath(directory)) == 
    +    return (os.path.normcase(os.path.realpath(directory)) ==
                os.path.normcase(os.path.realpath(os.path.join(directory, toplevel.decode()))))
      except subprocess.CalledProcessError:
        return False
    @@ -259,7 +259,7 @@ def multithread(function, list_of_arg_lists):
    def main(argv):
      deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
      verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
    -  skip_emsdk = bool(os.environ.get('GIT_SYNC_DEPS_SKIP_EMSDK', False))
    +  #skip_emsdk = bool(os.environ.get('GIT_SYNC_DEPS_SKIP_EMSDK', False))
      shallow = not ('--deep' in argv)
        
      if '--help' in argv or '-h' in argv:
    @@ -267,13 +267,13 @@ def main(argv):
        return 1
        
      git_sync_deps(deps_file_path, argv, shallow, verbose)
    -  subprocess.check_call(
    -      [sys.executable,
    -       os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
    -  if not skip_emsdk:
    -    subprocess.check_call(
    -        [sys.executable,
    -         os.path.join(os.path.dirname(deps_file_path), 'bin', 'activate-emsdk')])
    +  #subprocess.check_call(
    +  #    [sys.executable,
    +  #     os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
    +  #if not skip_emsdk:
    +  #  subprocess.check_call(
    +  #      [sys.executable,
    +  #       os.path.join(os.path.dirname(deps_file_path), 'bin', 'activate-emsdk')])
      return 0
        
        
    diff --git a/tools/window/BUILD.gn b/tools/window/BUILD.gn
    index 4a30186dbd..7dfa84af71 100644
    --- a/tools/window/BUILD.gn
    +++ b/tools/window/BUILD.gn
    @@ -38,7 +38,7 @@ skia_component("window") {
          "android/WindowContextFactory_android.h",
        ]
        libs += [ "android" ]
    -  } else if (is_linux) {
    +  } else if (is_linux || is_freebsd) {
        sources += [
          "unix/RasterWindowContext_unix.cpp",
          "unix/RasterWindowContext_unix.h",
    @@ -72,7 +72,7 @@ skia_component("window") {
        }
        if (is_android) {
          sources += [ "android/GLWindowContext_android.cpp" ]
    -    } else if (is_linux) {
    +    } else if (is_linux || is_freebsd) {
          sources += [
            "unix/GaneshGLWindowContext_unix.cpp",
            "unix/GaneshGLWindowContext_unix.h",
    @@ -123,7 +123,7 @@ skia_component("window") {
          if (skia_enable_graphite) {
            sources += [ "android/GraphiteVulkanWindowContext_android.cpp" ]
          }
    -    } else if (is_linux) {
    +    } else if (is_linux || is_freebsd) {
          sources += [
            "unix/GaneshVulkanWindowContext_unix.cpp",
            "unix/GaneshVulkanWindowContext_unix.h",
    @@ -178,7 +178,7 @@ skia_component("window") {
      }
        
      if (skia_use_dawn) {
    -    if (is_linux) {
    +    if (is_linux || is_freebsd) {
          if (dawn_enable_vulkan) {
            defines = [ "VK_USE_PLATFORM_XCB_KHR" ]
            libs += [ "X11-xcb" ]
    
    patch -p1 < ~/support-freebsd-skia-m142.diff
    
  3. 루트 디렉터리로 복귀:

    cd ../..
    

3. 시스템 의존성 설치

cargo build가 Skia C++ 코드를 컴파일하고 시스템 라이브러리에 링크할 수 있도록 모든 필수 개발 도구와 라이브러리를 설치합니다.

sudo pkg install rust ninja python3 gn \
    libglvnd libGLU \
    png icu harfbuzz freetype2

4. cargo build로 C 라이브러리 컴파일

cargo build를 실행하여 skia-bindings 패키지를 컴파일합니다. 이때 환경 변수를 사용하여 build.rs 스크립트가 FreeBSD 시스템 환경을 올바르게 인식하도록 설정합니다.

SKIA_GN_COMMAND="gn" \
SKIA_USE_SYSTEM_LIBRARIES="1" \
SKIA_GN_ARGS="extra_cflags+=[ \"-I/usr/local/include\", \"-I/usr/local/include/harfbuzz\", \"-I/usr/local/include/freetype2\" ]" \
cargo build -p skia-bindings --features "vulkan gl textlayout x11"
  • SKIA_GN_COMMAND="gn": build.rsgn을 다운로드하는 대신 시스템에 설치된 gn을 사용하도록 합니다.
  • SKIA_USE_SYSTEM_LIBRARIES="1": Skia의 내장(vendored) 라이브러리 대신 시스템 라이브러리(icu, freetype 등)를 사용하도록 하여 헤더 충돌을 방지합니다.
  • SKIA_GN_ARGS="...": gn에 C++ 컴파일 플래그를 전달하여 GL/gl.h, hb.h, ft2build.h 등 시스템 헤더 파일의 경로를 지정합니다.
  • --features "...": guiyom 툴킷에 필요한 GPU 백엔드(Vulkan, GL) 및 텍스트 레이아웃 기능을 활성화합니다.

5. C 라이브러리(.a) 활용

컴파일이 완료되면 (Finished ... 메시지 확인), guiyom 툴킷의 Makefile에서 사용할 두 개의 C 라이브러리 파일이 생성됩니다.

  • 생성 위치: target/debug/build/skia-bindings-[HASH]/out/skia/ (참고: [HASH] 값은 빌드마다 다릅니다.)

  • 산출물:

    1. libskia.a (약 101MB): Skia C++ 핵심 라이브러리
    2. libskia-bindings.a (약 8.4MB): C API 래퍼 라이브러리 (bindings.cpp)

Makefile 예시

guiyom 툴킷의 Makefile은 이 두 라이브러리를 모두 링크해야 합니다. C 코드(g-button.c 등)는 bindings.cpp의 함수 선언을 포함하는 C 헤더 파일(예: skia-c.h, 직접 작성 필요)을 #include 해야 합니다.

# cargo build의 중간 빌드 경로
SKIA_OUT_DIR = /path/to/rust-skia-m142/target/debug/build/skia-bindings-[HASH]/out/skia

# C API 헤더 경로
SKIA_INC_PATH = -I/path/to/guiyom/skia-c-headers

# 시스템 의존성 라이브러리
SYS_LIBS = -lfontconfig -lfreetype -lX11 -lGL \
           -lpng -lz -licuuc -lharfbuzz -lpthread -lm

# 최종 링크
guiyom: g-button.o other_objects.o ...
	$(CXX) g-button.o other_objects.o ... -o guiyom \
		-L$(SKIA_OUT_DIR) \
		-lskia-bindings \
		-lskia \
		$(SYS_LIBS)