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

2019.2.2 改標題linux

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

主體項目編譯前的操做(build.rs)

build.rs可實現本項目編譯前的額外操做,好比代碼生成、調用cmake/clang/gcc/ndk-build等編譯所依賴的C/C++庫、讀取C/C++頭文件生成FFI文件給Rust項目使用等等,至關於Rust寫的shell腳本。 爲了讓編譯過程更可控,一般輸出日誌表示經過了某一階段,或遇到什麼錯誤,Cargo支持build.rs編譯時輸出不一樣類型的語句,好比warning、error等,好比:ios

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

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

build.rs拉取git submodule

如下代碼摘自glsl-to-spirvgithub

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++代碼聲明起來,理論上支持正則匹配文件名與路徑shell

#![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-nativemacos

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.rswindows

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

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所需的全部目錄。好比,dir一、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的庫,其餘平臺也是如此。

相關文章
相關標籤/搜索