FreeBSD에서 Skia C 바인딩 라이브러리 생성 방법 (rust-skia 활용)
서론: C++ ABI 불일치 문제
이전 게시물(FreeBSD에서 Skia 컴파일하는 방법)에서는 gn과 ninja를 사용하여 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-skia의 cargo build 프로세스는 다음 작업을 수행합니다.
- Skia C++ 코어(
libskia.a)를gn으로 빌드합니다. gn이 사용한 C++ 컴파일 플래그를 캡처합니다.- C API 래퍼(
bindings.cpp)를 2단계에서 캡처한 동일한 플래그로 컴파일하여libskia-bindings.a를 생성합니다.
이 과정을 통해 libskia.a와 libskia-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 지원 패치를 적용해야 합니다.
-
Skia 하위 모듈 디렉터리로 이동:
cd skia-bindings/skia -
패치 적용: (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 -
루트 디렉터리로 복귀:
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.rs가gn을 다운로드하는 대신 시스템에 설치된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]값은 빌드마다 다릅니다.) -
산출물:
libskia.a(약 101MB): Skia C++ 핵심 라이브러리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)