Rust 交叉編譯與條件編譯總結

熊皮皮發表於2018-11-22

2019.2.2 改標題

文件列表見:Rust 移動端跨平臺複雜圖形渲染專案開發系列總結(目錄)

主體專案編譯前的操作(build.rs)

build.rs可實現本專案編譯前的額外操作,比如程式碼生成、呼叫cmake/clang/gcc/ndk-build等編譯所依賴的C/C++庫、讀取C/C++標頭檔案生成FFI檔案給Rust專案使用等等,相當於Rust寫的shell指令碼。 為了讓編譯過程更可控,通常輸出日誌表示通過了某一階段,或遇到什麼錯誤,Cargo支援build.rs編譯時輸出不同型別的語句,比如warning、error等,比如:

println!("cargo:warning=Error failed with {:?}.", some_reason);
複製程式碼

目前沒找到輸出info級別日誌的辦法,經實踐println!("cargo:info={:?}, some_status);無法在控制檯輸出資訊。

build.rs拉取git submodule

以下程式碼摘自glsl-to-spirv

use std::process::Command;

// Try to initialize submodules. Don't care if it fails, since this code also runs for
// the crates.io package.
let _ = Command::new("git")
    .arg("submodule")
    .arg("update")
    .arg("--init")
    .status();
複製程式碼

Cargo呼叫clang編譯所依賴的第三方C/C++庫

目前我看到比較完整的參考是官方的libstd/build.rs,編譯我們業務所需的第三方庫的命令幾乎都可以從那找到“靈感”,下面貼出核心程式碼段鎮宅,關鍵操作是build_libbacktrace(),通過cc::Build例項把需要編譯的C/C++程式碼宣告起來,理論上支援正則匹配檔名與路徑

#![deny(warnings)]

extern crate build_helper;
extern crate cc;

use build_helper::native_lib_boilerplate;
use std::env;
use std::fs::File;

fn main() {
    let target = env::var("TARGET").expect("TARGET was not set");
    if cfg!(feature = "backtrace") &&
        !target.contains("cloudabi") 
        // ... 更多條件
    {
        let _ = build_libbacktrace(&target);
    }

    if target.contains("linux") {
        // ... 一系列作業系統判斷及println!   
    }
}

fn build_libbacktrace(target: &str) -> Result<(), ()> {
    let native = native_lib_boilerplate("libbacktrace", "libbacktrace", "backtrace", "")?;

    let mut build = cc::Build::new();
    build
        .flag("-fvisibility=hidden")
        .include("../libbacktrace")
        .include(&native.out_dir)
        .out_dir(&native.out_dir)
        .warnings(false)
        .file("../libbacktrace/alloc.c")
        .file("../libbacktrace/backtrace.c")
        // ...一堆.c檔案

    let any_debug = env::var("RUSTC_DEBUGINFO").unwrap_or_default() == "true" ||
        env::var("RUSTC_DEBUGINFO_LINES").unwrap_or_default() == "true";
    build.debug(any_debug);

    if target.contains("darwin") {
        build.file("../libbacktrace/macho.c");
    } else if target.contains("windows") {
        build.file("../libbacktrace/pecoff.c");
    } else {
        build.file("../libbacktrace/elf.c");

        let pointer_width = env::var("CARGO_CFG_TARGET_POINTER_WIDTH").unwrap();
        if pointer_width == "64" {
            build.define("BACKTRACE_ELF_SIZE", "64");
        } else {
            build.define("BACKTRACE_ELF_SIZE", "32");
        }
    }

    File::create(native.out_dir.join("backtrace-supported.h")).unwrap();
    build.define("BACKTRACE_SUPPORTED", "1");
    build.define("BACKTRACE_USES_MALLOC", "1");
    build.define("BACKTRACE_SUPPORTS_THREADS", "0");
    build.define("BACKTRACE_SUPPORTS_DATA", "0");

    File::create(native.out_dir.join("config.h")).unwrap();
    if !target.contains("apple-ios") &&
       !target.contains("solaris") &&
       !target.contains("redox") &&
       !target.contains("android") &&
       !target.contains("haiku") {
        build.define("HAVE_DL_ITERATE_PHDR", "1");
    }
    build.define("_GNU_SOURCE", "1");
    build.define("_LARGE_FILES", "1");

    build.compile("backtrace");
    Ok(())
}
複製程式碼

Cargo呼叫ndk-build編譯第三方C/C++庫

以下程式碼參考自rustdroid-native

use std::{env, path::PathBuf, process};

fn main() {
    establish_ndk();
    establish_ndk_toolchain();
}

fn establish_ndk() {
    match find_ndk_path() {
        None => println!("cargo:warning=NDK path not found"),
        Some(path) => println!("cargo:warning=NDK path found at {}", path.to_string_lossy()),
    };
}

fn establish_ndk_toolchain() {
    match find_ndk_toolchain_path() {
        None => println!("cargo:warning=NDK_TOOLCHAIN path not found"),
        Some(path) => println!(
            "cargo:warning=NDK_TOOLCHAIN path found at {}",
            path.to_string_lossy()
        ),
    };
}

fn command_which_ndk_build_path() -> Option<PathBuf> {
    let mut cmd = process::Command::new("sh"); // mut due to API limitation
    cmd.arg("-c").arg("which ndk-build");
    match cmd.output() {
        Err(e) => {
            println!(
                "cargo:warning=Error executing process command <{:?}>: {}",
                cmd, e
            );
            None
        }
        Ok(o) => match String::from_utf8(o.stdout) {
            Err(e) => {
                println!("cargo:warning=Error parsing command output as UTF-8: {}", e);
                None
            }
            Ok(s) => PathBuf::from(&s)
                .parent()
                .and_then(|p| Some(p.to_path_buf())),
        },
    }
}

fn path_from_string(pathname: &str) -> Option<PathBuf> {
    // TODO: @@@ FUTURE RUST FEATURE
    //Some(PathBuf::from(pathname)).filter(|p| p.exists())
    let path = PathBuf::from(&pathname);
    if path.exists() {
        Some(path)
    } else {
        None
    }
}

fn path_from_env_var(varname: &'static str) -> Option<PathBuf> {
    match env::var(varname) {
        Ok(s) => path_from_string(&s),
        Err(_) => None,
    }
}

fn path_with_ndk_build(path: &PathBuf) -> Option<PathBuf> {
    // TODO: @@@ FUTURE RUST FEATURE
    //path.filter(|p| p.join("ndk-build").exists())
    if path.join("ndk-build").exists() {
        Some(path.clone())
    } else {
        None
    }
}

fn path_with_ndk_bundle_ndk_build(path: &PathBuf) -> Option<PathBuf> {
    path_with_ndk_build(&path.join("ndk-bundle"))
}

fn path_with_ndk_build_from_env_var(varname: &'static str) -> Option<PathBuf> {
    path_from_env_var(&varname).and_then(|p| path_with_ndk_build(&p))
}

fn path_with_ndk_bundle_ndk_build_from_env_var(varname: &'static str) -> Option<PathBuf> {
    path_from_env_var(&varname).and_then(|p| path_with_ndk_bundle_ndk_build(&p))
}

fn find_ndk_path_from_ndk_env_vars() -> Option<PathBuf> {
    // TODO: @@@ REFACTOR INTO ITERATION OF COLLECTION
    path_with_ndk_build_from_env_var("ANDROID_NDK_HOME").or_else(|| {
        path_with_ndk_build_from_env_var("ANDROID_NDK_ROOT").or_else(|| {
            path_with_ndk_build_from_env_var("NDK_HOME").or_else(|| {
                path_with_ndk_build_from_env_var("NDK_ROOT") // NVIDIA CodeWorks
                    .or_else(|| path_with_ndk_build_from_env_var("NDKROOT"))
            })
        })
    }) // NVIDIA CodeWorks
}

fn find_ndk_path_from_sdk_env_vars() -> Option<PathBuf> {
    // TODO: @@@ REFACTOR INTO ITERATION OF COLLECTION
    path_with_ndk_bundle_ndk_build_from_env_var("ANDROID_SDK_HOME")
        .or_else(|| path_with_ndk_bundle_ndk_build_from_env_var("ANDROID_SDK_ROOT"))
        .or_else(|| path_with_ndk_bundle_ndk_build_from_env_var("ANDROID_HOME"))
}

fn find_ndk_path_from_env_vars() -> Option<PathBuf> {
    find_ndk_path_from_ndk_env_vars().or_else(|| find_ndk_path_from_sdk_env_vars())
}

fn find_ndk_version_build_path(path: &PathBuf) -> Option<PathBuf> {
    //println!("cargo:warning=find_ndk_version_build_path() pathname: {:?}", pathname);
    if let Ok(iter) = path.read_dir() {
        for entry in iter {
            if let Ok(entry) = entry {
                let path = entry.path();
                //println!("cargo:warning=searching path: {:?}", path);
                if path.join("ndk-build").exists() {
                    return Some(path);
                }
            }
        }
    }
    None
}

fn find_ndk_path_from_known_installations() -> Option<PathBuf> {
    env::home_dir().and_then(|home| {
        path_with_ndk_bundle_ndk_build(
            // Android Studio on GNU/Linux
            &home.join(".android").join("sdk"),
        )
        .or_else(|| {
            path_with_ndk_bundle_ndk_build(
                // Android Studio on macOS
                &home.join("Library").join("Android").join("sdk"),
            )
        })
        .or_else(|| {
            find_ndk_version_build_path(
                // NVIDIA CodeWorks
                &home.join("NVPACK"),
            )
        })
    })
}

fn find_ndk_path() -> Option<PathBuf> {
    command_which_ndk_build_path()
        .or_else(|| find_ndk_path_from_env_vars())
        .or_else(|| find_ndk_path_from_known_installations())
}

fn find_ndk_toolchain_path() -> Option<PathBuf> {
    path_from_env_var("NDK_TOOLCHAIN")
}
複製程式碼

圖形開源專案build.rs參考編譯指令碼

Cargo編譯glslang

glslang-sys/build.rs

缺點:沒對應到最新的glslang專案。優點:使用檔案字尾匹配需要編譯的檔案,避免硬編碼八卦:此專案作者是Google員工,他還開發了cargo-lipo專案,極大地方便了Rust編譯iOS庫,剛接觸Rust時我啥都不懂,還給他提了一個錯誤的issue,導致Josh和他討論了一段時間。

glsl-to-spirv 直接用glslang自帶CMakeList.txt,此方案對於快速迭代且持續維護的開源專案是很好的選擇,降低build.rs編寫、維護成本。

glsl-to-spirv

Cargo編譯SPIRV-Cross

spirv_cross/build.rs

缺點:硬編碼參與編譯的檔案列表。優點:這是Josh的專案,工程組織上比前面glslang-sys專案更成熟,很值得參考。

Cargo編譯Metal Shader檔案到.metallib

metal/build.rs

編譯Metal的.shader檔案為.metallib,避免執行時編譯,提高效能。值得參考的地方是,如何在build.rs中呼叫XCode編譯工具鏈。

通過build.rs建立目錄

use std::fs;

fn main() {
    fs::create_dir_all("./dir1/dir2/dir3"); // 1
    fs::create_dir_all("./../lib"); // 2
}
複製程式碼
  • //1在build.rs同級目錄中建立出dir1/dir2/dir3所需的所有目錄。比如,dir1、dir2都不存在,則fs::create_dir_all()會自動建立它們,然後建立出dir3。
  • //2在build.rs上級目錄建立lib目錄。

結論:fs::create_dir_all()要注意路徑的區別。

參考:How to check if a directory exists and create a new one if it doesn't in Rust?

專案編譯後的操作

比如目前Rust專案還不支援直接編譯成iOS/macOS支援的.framework,我們還得用指令碼把.a和.h打包進.framework給客戶,如果有編譯後操作支援就非常棒了,遺憾的是,目前還沒有,經 @我傻逼我自豪(茶包) 兄提醒,這事已經在討論了cargo/issue

條件編譯

所有的條件編譯都由通過cfg配置實現,cfg支援any、all、not等邏輯謂片語合。

基本用法

在Cargo.toml中新增[features]段,然後列舉需要組合的feature名,大體上相當於gcc -條件1 -條件2 -條件3 ...

[features]
default = []
metal = ["gfx-backend-metal"]
vulkan = ["gfx-backend-vulkan"]
dx12 = ["gfx-backend-dx12"]
複製程式碼

mod級別條件編譯

實現示例,參考gl-rs/gl_generator/lib.rs

#[cfg(feature = "unstable_generator_utils")]
pub mod generators;
#[cfg(not(feature = "unstable_generator_utils"))]
mod generators;
複製程式碼

編譯特定CPU架構

指定target_arch + CPU架構名稱字串,如#[cfg(target_arch= "x86")]#[cfg(any(target_arch = "arm", target_arch = "x86"))]

參考libstd/os/android/raw.rs

#[cfg(any(target_arch = "arm", target_arch = "x86"))]
mod arch {
    use os::raw::{c_uint, c_uchar, c_ulonglong, c_longlong, c_ulong};
    use os::unix::raw::{uid_t, gid_t};

    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type dev_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type mode_t = u32;

    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type blkcnt_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type blksize_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type ino_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type nlink_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type off_t = u64;
    #[stable(feature = "raw_ext", since = "1.1.0")]
    pub type time_t = i64;
複製程式碼
#[doc(include = "os/raw/char.md")]
#[cfg(any(all(target_os = "linux", any(target_arch = "aarch64",
                                       target_arch = "arm",
                                       target_arch = "powerpc",
                                       target_arch = "powerpc64",
                                       target_arch = "s390x")),
複製程式碼

iOS/Android/macOS/Windows跨平臺編譯示例

[target.'cfg(any(target_os = "macos", all(target_os = "ios", target_arch = "aarch64")))'.dependencies.gfx-backend-metal]
git = "https://github.com/gfx-rs/gfx"
version = "0.1"
optional = true

[target.'cfg(target_os = "android")'.dependencies.gfx-backend-vulkan]
git = "https://github.com/gfx-rs/gfx"
version = "0.1"
optional = true

[target.'cfg(windows)'.dependencies.gfx-backend-dx12]
git = "https://github.com/gfx-rs/gfx"
version = "0.1"
optional = true
複製程式碼

編譯時指定例如cargo build --features metal --target aarch64-apple-ios --release可編譯relase版64位iOS靜態庫,同時將feature為gfx-backend-metal的程式碼打包進來(需要配置前面的features段)。

同理,cargo build --features vulkan --target aarch64-linux-android --release可編譯relase版64位Android靜態庫,同時將feature為gfx-backend-vulkan(需要配置前面的features段)。

編譯成指定型別二進位制包(.a/.so/.r)

目前還沒找到支援編譯出macOS/iOS支援的.framework辦法。

在Cargo.toml中新增[lib]段,

  • name表示輸出的庫名,最終輸出檔名為lib+name.a或lib+name.so比如libportability.so
  • crate-type表示輸出的二進位制包型別,比如
    • staticlib = .a iOS只認Rust輸出.a,Android可以.a和.so,配置成["staticlib", "cdylib"]在用cargo-lipo時會出警告不支援cdylib,忽略即可。
    • cdylib = .so
    • rlib = 給Rust用的靜態庫
    • dylib = 給Rust用的動態庫
  • path表示庫專案的入口檔案,通常是src/lib.rs,如果改動了這一位置,可通過path = 新位置實現,比如:
[lib]
name = "portability"
crate-type = ["staticlib", "cdylib"]
path = "src/ios/lib.rs"
複製程式碼

SDK開發的“售後服務”

提供.a/.so給業務團隊,這一過程可能會有人為失誤導致大家對接失敗,下面介紹些我們使用的小技巧。

讀取.a靜態庫的iOS版本

在macOS terminal執行如下命令,用/查詢VERSION

otool -lv xyz.a | less
複製程式碼

參考:check-ios-deployment-target-of-a-static-library

nm檢視匯出符號

有時編碼疏忽導致沒給需要匯出的C介面新增#[no_mangle]extern等修飾,或者使用了不合理的優化attribute導致符號被優化掉,此時業務連結我們的庫就會失敗,因此,交付二進位制包前用nm確認符號表是合格的工程師習慣。參考:How do I list the symbols in a .so file。以下為macOS示例程式碼。

nm檢視.so匯出符號

nm -D ./target/release/libportability.so  | grep fun_call_exported_to_c
0000000000003190 T fun_call_exported_to_c
複製程式碼

nm檢視.a匯出符號

nm -g ./target/release/libportability.a  | grep glActiveTexture
000000000000190c T _glActiveTexture
複製程式碼

Rust匯出C介面的正確姿勢

The Rust philosophy is to prefer explicit over implicit. Rust will only export symbols that are publicly accessible from the root crate. This makes it very easy to inspect the public interface of a crate without crawling through all files: just follow the pub from the root. In your case, the symbol rle_new is publicly accessible to anyone having access to the rle module (such as sibling modules), but the rle module itself is not publicly accessible in the root crate.

The simplest solution is to selectively export this symbol:

pub use rle::rle_new;
複製程式碼

stackoverflow.com/questions/4…

因此,對於在非lib.rs中標識#[no_mangle]的函式,如果忘了在lib.rs中pub use它,打包成C庫或rlib還是找不到且出現如下編譯警告。解決辦法就是在lib.rs中要麼pub use 模組::*pub use 模組::{符號名1, 符號名2}

warning: function is marked #[no_mangle], but not exported
   --> src/portability/gl_es/src/c_abi/mod.rs:785:1
    |
785 | / pub extern "C" fn glViewport(x: GLint, y: GLint, width: GLsizei, height: GLsizei) {
786 | |     unimplemented!()
787 | | }
    | |_^
    |
    = help: try exporting the item with a `pub use` statement
複製程式碼

檢視本機rust編譯器可編譯的系統列表

rustc --print target-list
複製程式碼

比如,rustc --print target-list | grep ios沒有內容,得用rustup component add ios相關的CPU架構,然後才能交叉編譯iOS的庫,其他平臺也是如此。

相關文章