# MemeDock：一个纯 Vibe Coding 的表情包管理器

原文：https://blog.grtsinry43.com/posts/memedock-app/

作者：grtsinry43

发布时间：2026-10-23T16:04:32Z

内容更新时间：2026-10-30T05:52:44Z

说来也是奇怪，写这个项目的原因是和同事斗图的时候感觉不是很方便，尤其是表情还很分散不好找到。。所以有了这个很离谱的项目。

写下这篇文章的时候，我刚刚还在忙着想办法写 iOS 平台，然后把想要的功能迁过去，顺手看了一下提交历史，第一次提交是在 10 月 5 号，到今天（10 月 23 号）正好 18 天。

这十几天的业余时间里，我写了 Rust 的核心，通过 UniFFI 编译到各个平台，之后陆陆续续把 **Android（Jetpack Compose）、Linux（GTK4 / Libadwaita）、macOS（SwiftUI / AppKit）、和正在写的 iOS（SwiftUI）和 Windows（WinUI 3）** 五个端的原生界面大概完善好，同时用 `Axum` + `SvelteKit` 写了个自建的同步服务/WebUI

https://github.com/grtsinry43/MemeDock

说实话，如果是半年前，要我一个人在两三周里同时写 Rust、Kotlin、Swift、C#、GTK4 外加一套 C/C++ 混合交叉编译的媒体管线，那我肯定直接放弃了。但是时代变了嘛。毕竟现在有了 AI 之后，说实话，我会觉得，在整个我的工作流定型之后，写这样一个项目，感觉还是比较可控的

[我是怎样使用 AI Agent 写代码的](<https://blog.grtsinry43.com/posts/coding-with-ai-agent>)

从 AI 辅助编码、Vibe Coding，到现在的混合开发工作流。


简单来说，这篇文章会讲两个方面。 一是我在技术选型上纠结了半天，为什么最后选了「Rust 核心共享 + 各端纯原生」这么一条路，以及从 UniFFI 桥接到五个平台编译、打包、链接的完整实现细节；二是我自己怎么配合 AI 去推进这样一个多端项目。

---

## 为什么要做这个项目

理由很简单，我的表情包太乱了。~~最近摸鱼和同事发表情包的时候，~~ 感觉找起来很麻烦，而且好多软件会有格式上的各种限制，比如说 WebM、WebP 这种的兼容性也不好，也有软件发送 PNG 之后会被强制转为黑底。 

所以，当时我想的功能大概是这样的：

1. 存储：所有导进来的图按 `SHA-256` 去重存在本地，原文件只读；
2. 表情导入：用一个 Telegram 贴纸包链接就能直接抓取预览，自动把 `.tgs` 和 `.webm` 转成带透明通道的动态 WebP；支持剪贴板粘贴、文件拖入和 Android 系统分享；以后还可能，比如说扫本地的 QQ 的缓存文件，或者其他软件的文件来导入。 
4. 发图的时候按需转换：在复制、拖拽或者调起系统分享的时候，可以根据设置实时转出原图、兼容 PNG（1024px）、白底 PNG、小尺寸 JPEG（512px）或者兼容 GIF。

功能想清楚了，所以该选什么技术栈好呢？ 

---

## 技术选型：为什么选择各端原生呢？ 

在这个项目的技术栈上，我其实犹豫了非常久。

其实我个人还是在前端上占比比较多，但是各个语言都算有所涉猎吧。整个大概的语言分布，就按 GitHub 的来说是这样：

![](https://readme-stat-kappa.vercel.app/api/top-langs/?username=grtsinry43&layout=compact&theme=tokyonight&hide_border=true&border_radius=10&hide=html,css,jupyter,Jupyter%20Notebook&bg_color=0D1117&langs_count=8)

去年写 RSS 阅读器 `PureFlow` 的时候，我用的是 Kotlin Multiplatform (KMP) + Compose Multiplatform。当时觉得一套代码同时编译到 Android、iOS 和桌面端确实很爽。

[PureFlow 简析：用 Kotlin 跨平台构建 RSS 阅读器](<https://blog.grtsinry43.com/posts/kmp-pureflow>)

基于 Kotlin Multiplatform 和 Compose Multiplatform 技术栈，一套代码同时支持多平台的早期探索。


但是对于这个需求，每个分析下来是这样的。

首先是 **Webview 方案（Electron / Tauri）**。这个最先排除。好吧，我一点都不喜欢 WebView。作为一个~~稍微有点追求的开发者~~，我还是不希望用 WebView 来跨平台。这个东西理论上性能确实高，但是它占的内存，包括原生体验，我觉得都是比较难受的。 

然后是 **KMP (Compose Multiplatform) 和 Flutter**。KMP 的问题是，它在桌面上整个跑在 JVM 平台上，本来就比较笨重；然后调原生的能力其实也就在桌面上，还是很难受的，和系统的配合也不是很好。主要最大的问题还是冷启动吧，就是在我想要使用这个软件的时候，要开一个很重的 JVM，这个无法接受。 

而对于 Flutter 来说，其实我是比较讨厌它调那些原生方法，要做各种的 channel，然后写起来也十分的麻烦嘛。如果你在它的给的范围内用 Dart 来写，无论是逻辑还是 UI 还是很舒服的。但是，一旦涉及到这种原生操作，其实 Flutter 的体验还是没有那么好的。 

而且，表情包管理工具在每个平台上的交互习惯完全不一样：

- 在 macOS 和 Windows 上，核心操作是快捷键呼出、直接把图片文件从窗口里拖出去（Drag & Drop）、写入系统剪贴板；
- 在 Android 和 iOS 上，核心操作是系统相册选择器、底部标签栏、下拉手势，以及调起系统的分享面板（Share Sheet）；
- 在 Linux（Wayland / GNOME）上，我希望能直接融入 Libadwaita 的原生视觉和传输协议。

。。好吧，这样看来纠结了一圈，最后还是决定干脆 UI 不做跨平台，而用 Rust 做一个共享的核心，然后各个端都使用自己的原生框架去写。只要我们逻辑共享就可以了

| 方案 | Electron / Tauri | Flutter / KMP (Compose) | **Rust Core + UniFFI + 各端原生 UI** |
| :--- | :--- | :--- | :--- |
| **运行开销与体积** | 偏重，带着 Webview 的固有开销 | 中等，自带渲染引擎和运行时 | **最轻**，无 GC 核心 + 系统自带 UI |
| **调系统原生能力（拖拽/剪贴板/分享）** | 受限于浏览器 API，需要折腾桥接 | 调原生接口比较繁琐，桌面端支持一般 | **最直接**，本身就是系统原生代码 |
| **各端界面体验** | 网页感重 | 移动端还行，桌面端还是有割裂感 | **原汁原味**（SwiftUI / GTK4 / Compose / WinUI 3） |
| **接入 C/C++ 媒体库** | 走 Node 原生模块或拉子进程 | 需要跨多层 JNI / C 互操作 | **Rust 直接静态链接**，进程内统一管理 |
| **以前手写的维护成本** | 低 | 中 | **极高**，一个人很难同时顾得过来 |
| **现在配合 AI 的成本** | 低 | 中 | **可控**，核心规范在 Rust，原生 UI 交给 AI 写很快 |

说真的，我不得不再次感叹 AI。如果放在以前，个人开发者选最后一条路的话，就是死路一条。就不说别的，在语言之间切换的上下文就足够痛苦了。但现在有 AI Agent，情况就不太一样了。只要我把数据模型、SQLite 存储、文件哈希去重、Telegram 转码、任务取消和错误处理在 Rust 实现好，那 AI 只管绘制 UI、展现状态，然后接入原生 API 了。 

而 SwiftUI、Jetpack Compose、WinUI 3 这些声明式 UI，恰好是现在 Claude 和 Codex 最擅长写的部分呀

---

## 从打地基开始

那就先从项目结构开始吧。整个仓库是一个 Cargo workspace，大概的结构是这样的

```text
MemeDock/
├── crates/
│   ├── domain/          # 强类型 ID、模型、导出规则与契约（不依赖数据库、异步运行时和图像库）
│   ├── storage/         # SQLite (SeaORM)、原子文件暂存 (StagedFile) 与 SHA-256 内容寻址存储
│   ├── core/            # 业务用例、任务调度、媒体转码管线、Telegram 导入、备份归档与同步逻辑
│   ├── ffi/             # 给 Swift / Kotlin / C# 用的 UniFFI 绑定、DTO 转换与事件流
│   └── api-contracts/   # 同步服务端与客户端共享的数据结构
├── tools/
│   ├── native-media/    # FFmpeg / libvpx / libwebp / rlottie 交叉编译脚本
│   ├── uniffi-bindgen/  # 锁定版本的 Kotlin / Swift 绑定生成器
│   ├── macos/           # macOS 打包、图标生成与 Mach-O 对齐修复脚本
│   ├── ios/             # iOS 静态库合并与 XCFramework 构建脚本
│   └── windows/         # Windows vcpkg overlay 与 C# 桥接构建脚本
└── apps/
    ├── android/         # Kotlin + Jetpack Compose + 自研 Gradle 构建插件
    ├── linux/           # Rust + GTK4 + Libadwaita（不走 FFI，直接调 memedock-core）
    ├── macos/           # Swift 6 + SwiftUI & AppKit
    ├── windows/         # C# + WinUI 3 (Preview) + uniffi-bindgen-cs
    ├── ios/             # Swift 6 + SwiftUI (进行中)
    └── server/          # Axum + 内嵌 SvelteKit 前端（自建同步服务）
```

在讲各个平台怎么编译之前，先看两个前提条件吧。

### 1. 编译器规则

现在有了 AI，写 Rust 会简单一些；但是 AI 很多情况下都喜欢为了图省事到处塞 `.unwrap()`，或者在遇到生命周期问题时乱上 `unsafe`。 这里如果错误处理搞不好，那客户端就闪退了呀。

所以这里简单做了下规范吧

```toml
[workspace.lints.rust]
unsafe_code = "forbid"

[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
```

### 2. Telegram 贴纸转码

MemeDock 支持直接使用 Telegram 的贴纸包链接或者名字，然后按需选择导入

Telegram 的贴纸有三种：普通的静态 `.webp`、用 Gzip 压缩的 Lottie 矢量动画 `.tgs`，还有 VP9 编码的 `.webm` 视频。 于是，这里要维护一个逻辑在 `crates/core/src/images/telegram.rs` 里把 `.tgs` 和 `.webm` 统一转成了**带透明通道的动态 WebP**。

这里转码的能力来源于 FFmpeg。针对它进行了裁剪，打包进项目。这样就站在前人的肩膀上，完成了方便的转换。 

---

## 跨平台桥接与编译

聊完 Rust 核心，接下来就是整个项目最折腾的部分了，我们需要把 Rust 产物编译到各个平台能直接用的样子。 

### 第一步：设计 `crates/ffi` 与 UniFFI 绑定机制

首先是 `crates/ffi/Cargo.toml`。因为五个平台对链接方式的要求完全不同，我给 `memedock-ffi` 同时开了三种 `crate-type`：

```toml
# crates/ffi/Cargo.toml
[package]
name = "memedock-ffi"
version.workspace = true

[lib]
crate-type = ["rlib", "cdylib", "staticlib"]

[dependencies]
memedock-core = { path = "../core" }
memedock-domain = { path = "../domain" }
tokio = { workspace = true, features = ["macros"] }
uniffi.workspace = true
```

这三种类型分别对应不同的端：
- **`rlib`**：给纯 Rust 写的 Linux 客户端直接源码级引用；
- **`cdylib`**：给 Android（`.so`）、macOS（`.dylib`）和 Windows（`.dll`）做动态库加载；
- **`staticlib`**：给 iOS（`.a`）做静态库归档，最后打进 `.xcframework`。

在写 FFI 导出接口的时候，可以直接用 UniFFI 的过程宏。在 `crates/ffi/src/lib.rs` 结尾加上一行 `uniffi::setup_scaffolding!()`，然后把要暴露给原生端的对象和 DTO 标上 `#[derive(uniffi::Object)]`、`#[derive(uniffi::Record)]` 和 `#[uniffi::export]`：

```rust
// crates/ffi/src/library.rs
#[derive(uniffi::Object)]
pub struct LibraryHandle {
    pub(crate) inner: memedock_core::Library,
}

#[uniffi::export]
pub async fn open_library(configuration: LibraryConfiguration) -> Result<Arc<LibraryHandle>> {
    Ok(Arc::new(LibraryHandle {
        inner: memedock_core::Library::open(configuration.try_into()?).await?,
    }))
}

#[uniffi::export]
impl LibraryHandle {
    pub fn request_thumbnail(&self, id: String, priority: Priority) -> Result<Arc<ThumbnailTask>> {
        Ok(ThumbnailTask::new(
            self.inner.request_thumbnail(id.parse()?, priority.into())?,
        ))
    }

    pub fn export_original(&self, id: String) -> Result<Arc<ExportTask>> {
        Ok(ExportTask::new(self.inner.export_original(id.parse()?)?))
    }
    // ...
}
```

我们这里要刻意地保持 **FFI 边界只传文件路径和轻量元数据 DTO，绝不传图像像素 Buffer。** 比如上面这个 `request_thumbnail`，它返回的 `ThumbnailTask` 最终给到 Kotlin / Swift / C# 的只是一个本地生成的缩略图文件路径，而不是几兆的 RGBA 字节数组。这样跨语言边界几乎没有内存拷贝开销，上层拿到路径后直接喂给系统的 `Coil`（Android）或 `ImageIO`（iOS/macOS）去解码上屏。

因为用了过程宏，编译出来的 `.so` / `.dylib` / `.a` / `.dll` 二进制文件内部就已经自带了完整的接口元信息（Metadata）。 可以封装一个非常简单的入口来干这件事情

```rust
// tools/uniffi-bindgen/src/main.rs
fn main() {
    uniffi::uniffi_bindgen_main();
}
```

---

### 第二步：把 FFmpeg / libvpx / libwebp / rlottie 交叉编译成静态库

在编译 `memedock-ffi` 之前，前置需要的是四个 C/C++ 媒体库编译出来。

在类 Unix 平台（Linux、Android、macOS、iOS）上，可以写一个 `tools/native-media/build.sh` 。它会先根据传入的 Rust target 识别平台架构和编译器工具链：

```bash
# tools/native-media/build.sh
case "$target" in
  x86_64-unknown-linux-gnu) arch=x86_64; vpx_target=x86_64-linux-gcc; abi= ;;
  aarch64-linux-android) arch=aarch64; vpx_target=arm64-linux-gcc; abi=arm64-v8a ;;
  x86_64-linux-android) arch=x86_64; vpx_target=x86_64-linux-gcc; abi=x86_64 ;;
  aarch64-apple-darwin) arch=arm64; vpx_target=arm64-darwin23-gcc; abi=; darwin=1 ;;
  x86_64-apple-darwin) arch=x86_64; vpx_target=x86_64-darwin23-gcc; abi=; darwin=1 ;;
  aarch64-apple-ios) arch=arm64; vpx_target=arm64-darwin-gcc; abi=; darwin=1; ios=1; sdk=iphoneos ;;
  aarch64-apple-ios-sim) arch=arm64; vpx_target=arm64-iphonesimulator-gcc; abi=; darwin=1; ios=1; sdk=iphonesimulator ;;
esac
```

同时，为了不让安装包体积膨胀，FFmpeg 的 `configure` 参数被我砍到了只剩解码 WebM 贴纸必需的几个模块：

```bash
"$base/sources/ffmpeg/configure" ${ffmpeg_args[@]+"${ffmpeg_args[@]}"} --prefix="$prefix" \
  --disable-everything --disable-autodetect --disable-programs --disable-doc --disable-debug \
  --disable-avdevice --disable-swresample --disable-network --disable-shared \
  --enable-static --enable-pic --enable-libvpx --enable-decoder=libvpx_vp8,libvpx_vp9 \
  --enable-avfilter --enable-filter=fps \
  --enable-demuxer=matroska --enable-parser=vp8,vp9 --enable-protocol=file --disable-x86asm
```

首先 `--disable-everything`，只开 `matroska` 解复用、`libvpx_vp8/vp9` 解码，再加一个 `fps` 滤镜（用来修复部分 Telegram WebM 贴纸里时间戳重复导致的播放鬼畜问题）。

---

### 第三步：各个平台的编译和接入

媒体库编好之后，然后就是想办法放到各个平台了。 

#### 1. Linux 端（GTK4 + Libadwaita）

Linux 端（`apps/linux`）本身就是用 `gtk4-rs` 和 `libadwaita-rs` 写的 Rust 程序，所以它根本不需要走 `memedock-ffi`，这个最简单了，直接在 `Cargo.toml` 里把 `memedock-core` 引进来就行：

```toml
# apps/linux/Cargo.toml
[package]
name = "memedock-linux"

[[bin]]
name = "memedock"
path = "src/main.rs"

[dependencies]
memedock-core = { path = "../../crates/core" }
memedock-domain = { path = "../../crates/domain" }
gtk4.workspace = true
libadwaita.workspace = true
```

编译的时候一行命令就完事了：

```bash
cargo build --release --locked -p memedock-linux
```

Cargo 会 管理前面的构建流程，把 FFmpeg、libvpx、rlottie 的 `.a` 静态库和 `memedock-core` 一起链接进单个原生可执行文件 `target/release/memedock` 里。配合 `packaging/arch/` 下的 `PKGBUILD` 打成包，在 GNOME / Wayland 下跑起来跟系统自带应用没有任何区别。

---

#### 2. Android 端（Jetpack Compose）：自定义 Gradle 插件

思路依然是把前面构建的部分交给构建系统管理，那安卓就交给 Gradle 管理，所以这里在 `apps/android/build-logic` 写了一套 Gradle 插件（`MemeDockRustPlugin`）

```kotlin
// apps/android/build-logic/src/main/kotlin/RustBuildTasks.kt
val args = mutableListOf(cargoNdk.get().asFile.absolutePath, "ndk")
for (abi in abis.get()) args.addAll(listOf("--target", abi))
args.addAll(listOf(
    "--platform", "28",
    "--output-dir", outputDirectory.get().asFile.absolutePath,
    "build", "--locked", "-p", "memedock-ffi"
))
if (release.get()) args.add("--release")

exec.exec {
    workingDir(repository)
    environment("ANDROID_NDK_HOME", ndk.absolutePath)
    // Android 15+ 要求原生库按 16 KB 页面大小对齐
    environment("CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384")
    environment("CARGO_TARGET_X86_64_LINUX_ANDROID_RUSTFLAGS", "-C link-arg=-Wl,-z,max-page-size=16384")
    environment("CARGO", "cargo")
    commandLine(args)
}.assertNormalExitValue()
```

`.so` 编好之后，`GenerateBindingsTask` 会调用宿主机编出来的 `memedock-bindgen` 生成 Kotlin 代码，然后自动挂进 Android Library 的源码目录里：

```kotlin
// apps/android/build-logic/src/main/kotlin/MemeDockRustPlugin.kt
variant.sources.kotlin?.addGeneratedSourceDirectory(generated) { it.outputDirectory }
variant.sources.jniLibs?.addGeneratedSourceDirectory(native) { it.outputDirectory }
```

这样上层的 Jetpack Compose 模块只需要依赖 `:rustBridge`，就能直接像调普通 Kotlin 挂起函数一样调 Rust 核心了。

很爽吧，这就是丝滑的混合开发

---

#### 3. macOS 端（SwiftUI & AppKit）

macOS 端（`apps/macos`）用 Swift Package Manager 管理

```swift
// apps/macos/Package.swift
targets: [
    .systemLibrary(name: "MemeDockBridgeFFI", path: "Generated/FFI"),
    .target(
        name: "MemeDockBridge",
        dependencies: ["MemeDockBridgeFFI"],
        path: "Generated/Bridge"
    ),
    .executableTarget(
        name: "MemeDock",
        dependencies: ["MemeDockBridge"],
        path: "Sources/MemeDock",
        linkerSettings: [
            .linkedLibrary("memedock_ffi")
        ]
    ),
]
```

这里我们写了一个打包脚本， `tools/macos/build.sh` 先用 Cargo 编译出 `libmemedock_ffi.dylib`，再用 `memedock-bindgen` 从这个 `.dylib` 生成 `MemeDockBridge.swift`、`MemeDockBridgeFFI.h` 和 `module.modulemap` 放进 `Generated/` 目录，然后调 `swift build` 编译出可执行文件。

---

#### 4. iOS 端（SwiftUI）：用 `libtool` 缝合静态库与双架构 `XCFramework`

到了 iOS 端（`apps/ios`），这里不打算折腾动态库签名，而是直接走 `staticlib`（静态库 `.a`）打包进 `XCFramework`。

在 `tools/ios/build-bridge.sh` 里，脚本会针对真机（`aarch64-apple-ios`）和模拟器（`aarch64-apple-ios-sim`）分别调 `cargo rustc` 编译静态库：

```bash
# tools/ios/build-bridge.sh
SDKROOT="$sdkroot" IPHONEOS_DEPLOYMENT_TARGET=18.0 \
  "CC_$target=$(xcrun --sdk "$sdk" --find clang)" \
  "CXX_$target=$(xcrun --sdk "$sdk" --find clang++)" \
  "CFLAGS_$target=-target $clang_target -isysroot $sdkroot" \
  "CXXFLAGS_$target=-target $clang_target -isysroot $sdkroot" \
  BINDGEN_EXTRA_CLANG_ARGS="-target $clang_target -isysroot $sdkroot" \
  cargo rustc --locked -p memedock-ffi --lib --crate-type staticlib --target "$target"
```

这样 Xcode 里只需要引用 `MemeDockNative.xcframework` 和生成的 `MemeDockBridge.swift`，不管切模拟器还是切真机，一行额外配置都不需要改。

---

#### 5. Windows 端（C# + WinUI 3）：vcpkg Overlay 补丁与 `uniffi-bindgen-cs`

最后是 Windows 端（`apps/windows`）。在 Windows 上用 MSVC 工具链编译 FFmpeg 和 `rlottie`，如果去拉 MSYS2 跑 bash 脚本会非常慢且容易出环境问题，所以我用了 **vcpkg + Overlay Ports** 的方式。

在 `tools/windows/prepare-media.ps1` 里，脚本会拉取固定 commit 的 vcpkg，然后动态修改 `ffmpeg` 的 `portfile.cmake`，把前面那套 `--disable-everything` 的最小化解码参数塞进去：

```powershell
# tools/windows/prepare-media.ps1
$originalOptions = 'set(OPTIONS "--enable-pic --disable-doc --enable-runtime-cpudetect --disable-autodetect")'
$minimalOptions = 'set(OPTIONS "--disable-everything --disable-network --enable-pic --disable-doc --enable-runtime-cpudetect --disable-autodetect")'
$ffmpegPort = $ffmpegPort.Replace($originalOptions, $minimalOptions)

$decoderOptions = 'string(APPEND OPTIONS " --enable-decoder=libvpx_vp8,libvpx_vp9 --enable-demuxer=matroska --enable-parser=vp8,vp9 --enable-protocol=file --enable-filter=fps --disable-x86asm")'
$ffmpegPort = $ffmpegPort.Replace($optionsLog, $decoderOptions + "`n" + $optionsLog)
```

而在编译 Rust 的时候，crates.io 上的 `rlottie 0.5.4` 源码里居然无条件写了一行 `use std::os::unix::ffi::OsStrExt;`，在 Windows 下直接编译失败。所以我把 `rlottie` 源码放进了仓库的 `third-party/rlottie`，修掉那行 Unix 引用后，在根 `Cargo.toml` 里用 `[patch.crates-io]` 做了替换。

等 `cargo build -p memedock-ffi --target x86_64-pc-windows-msvc` 编出 **`memedock_ffi.dll`** 之后，再用 `uniffi_bindgen_cs` 生成 `memedock_ffi.cs`。而在 C# 的桥接工程 `MemeDock.Bridge.csproj` 里，直接把生成的 `.cs` 文件和运行时的 `.dll` 挂进去：

```xml
<!-- apps/windows/bridge/MemeDock.Bridge.csproj -->
<ItemGroup>
  <Compile Include="$(NativeRoot)\generated\memedock_ffi.cs" Link="Generated\memedock_ffi.cs" />
  <None Include="$(NativeRoot)\runtime\*.dll" Link="%(Filename)%(Extension)"
        CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
```

这样上层的 WinUI 3 应用只要引用 `MemeDock.Bridge.csproj`，编译输出目录里就会自动带上 `memedock_ffi.dll` 和裁剪后的 FFmpeg `.dll`，C# 代码直接通过 P/Invoke 调用底层的 Rust 核心。

---

### 五端编译与交付产物汇总

。。。讲到这里，已经打得手指酸了
不过我们确实实现了一套核心代码处处运行

| 平台 | 媒体库编译方式 | Rust 产物类型 | 绑定生成方式 | 最终交付给 UI 工程的桥接产物 |
| :--- | :--- | :--- | :--- | :--- |
| **Linux** | `build.sh` 编为静态 `.a` | `rlib` (源码直连) | 无需 FFI，直接调 `memedock-core` | 单个原生二进制 `memedock-linux` |
| **Android** | `build.sh` + NDK 编为 `.a` 并静态合入 | `cdylib` (`libmemedock_ffi.so`) | Gradle 插件调 `memedock-bindgen` | `jniLibs/<abi>/*.so` + 生成的 Kotlin (JNA) 源码 |
| **macOS** | `build.sh` 编为 `.a` 并静态合入 | `cdylib` (`libmemedock_ffi.dylib`) | `memedock-bindgen` | 修好 Mach-O 对齐与 `@rpath` 的 `.dylib` + Swift/C 头文件 |
| **iOS** | `build.sh` + `xcrun` 编为 `.a` | `staticlib` (`libmemedock_ffi.a`) | `libtool` 合并 `.a` + `memedock-bindgen` | 双架构 `MemeDockNative.xcframework` + Swift 源码 |
| **Windows** | `vcpkg` + Overlay Ports (MSVC) | `cdylib` (`memedock_ffi.dll`) | `memedock-windows-bindgen` (`uniffi_bindgen_cs`) | `memedock_ffi.dll` + 媒体 `.dll` + `MemeDock.Bridge.dll` |

![](https://raw.githubusercontent.com/grtsinry43/MemeDock/main/assets/screenshots/hero-desktop.webp)

> MemeDock 各端实际运行截图
https://raw.githubusercontent.com/grtsinry43/MemeDock/main/assets/screenshots/telegram-import.webp
https://raw.githubusercontent.com/grtsinry43/MemeDock/main/assets/screenshots/batch-organize.webp
https://raw.githubusercontent.com/grtsinry43/MemeDock/main/assets/screenshots/output-backup-settings.webp
https://raw.githubusercontent.com/grtsinry43/MemeDock/main/assets/screenshots/android-overview.webp


---

## Vibe Coding 过程

emm 就是大概的开发节奏吧

> MemeDock 18 天开发记录
2026-10-05|搭建地基与 Android 首版|完成 domain / storage / core / ffi 四层结构，接通 UniFFI 和 Android 基础图库
2026-10-06|跑通完整功能闭环|做好合集标签、回收站、导出预设、单文件备份恢复与多语言
2026-10-07|Linux 客户端与批量操作|写完 GTK4 + Libadwaita 客户端，加上多选与批量归类
2026-10-09|Telegram 贴纸包导入|搞定 FFmpeg + rlottie 静态编译，实现 .tgs / .webm 转动态 WebP
2026-10-12|macOS 客户端与打包|适配 Apple Darwin 媒体栈，完成 SwiftUI 桌面端、快捷键与 DMG 打包工作流
2026-10-16|Windows WinUI 3 客户端|搞定 MSVC 下媒体库构建，接通 C# 桥接、Fluent 界面与剪贴板/拖出
2026-10-18|自建同步服务端|用 Axum + SvelteKit 写好同步服务与管理后台，四端接通增量同步
2026-10-23|推进 iOS 客户端|打包双架构 XCFramework，完成本地导入、搜索、详情预览与系统标签栏框架



## 写在最后

以前一个人写项目，总得在“套壳凑合用”和“没精力写多端原生”之间妥协。现在有了 AI 帮忙分担各端 UI 和构建脚本的体力活，确实一个人维护的成本低了很多

目前 MemeDock 的 macOS、Linux、Android 都已经发了 Release，iOS 端还在继续收尾系统分享和整理功能 😋。如果你平时也有攒表情包的习惯，或者对这套跨平台方案感兴趣，欢迎去 GitHub 试试看，觉得好用的话也可以点个 Star~

https://github.com/grtsinry43/MemeDock

