在 FreeBSD 上生成 Skia C 绑定库的方法 (利用 rust-skia)

引言:C++ ABI 不兼容性问题

前一篇帖子(如何在 FreeBSD 上编译 Skia)介绍了使用 gnninja 直接编译 Skia C++ 库 (libskia.a) 的方法。该方法适用于运行像 viewer 这样在 gn 构建系统内集成的示例。

然而,当尝试将 libskia.a 库链接到使用 MakefileCMake 的外部 C/C++ 项目时,就会出现 C++ ABI (Application Binary Interface) 不兼容性问题。

  • 原因: Skia 的 C++ 构建系统 (gn) 会定义诸如 skia_use_vulkan=true 等预处理器标志 (-D...)。像 DisplayParams.h 这样的 Skia 头文件会根据这些标志动态改变结构体的内存布局(大小)。
  • 问题: 使用 Makefile 编译的 C 项目不知道 gn 使用的这些标志,因此导致库 (libskia.a) 和 C 项目代码中相同结构体的大小不一致,引发 Invalid readSIGSEGV(内存访问冲突)。

利用 rust-skia 的 C API 解决方案

rust-skia 项目提供了一个 C API 桥接,用于解决这个 ABI 问题。rust-skiacargo build 过程执行以下操作:

  1. 使用 gn 编译 Skia C++ 核心库 (libskia.a)。
  2. 捕获 gn 使用的 C++ 编译标志。
  3. 使用第 2 步中捕获的相同标志编译 C API 封装 (bindings.cpp),生成 libskia-bindings.a

通过此过程,libskia.alibskia-bindings.a 成为一套 ABI 兼容的 C 库集合。

本文将说明如何利用 rust-skia 的构建系统生成 Skia C 库 (libskia.a, libskia-bindings.a) 以用于 C 工具包开发。


1. 下载 rust-skia 源代码

必须使用 git 克隆包含 skia 子模块的源代码。

# 基于 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 包。此时,必须使用 环境变量 设置 FreeBSD 系统环境,以确保 build.rs 脚本正确识别。

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,而非下载。
  • SKIA_USE_SYSTEM_LIBRARIES="1":使用系统库 (icu, freetype 等),而非 Skia 的内置库,以避免头文件冲突。
  • SKIA_GN_ARGS="...":传递 C++ 编译标志给 gn,指定系统头文件路径,例如 GL/gl.hhb.hft2build.h
  • --features "...":激活 guiyom 工具包所需的 GPU 后端 (Vulkan, GL) 和文本布局功能。

5. C 库 (.a) 的利用

编译成功完成后 (Finished ... 消息确认),将生成两个 C 库文件,供 guiyom 工具包的 Makefile 使用。

  • 生成位置: 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)必须 #include 包含 bindings.cpp 函数声明的 C 头文件(例如 skia-c.h,需要手动创建)。

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