Rust编写Switch应用

13 6月

写Switch应用首选肯定是C/C++语言,主要依赖是devkitpro和libnx。

如果要用Rust语言来写,最核心的就是两个问题,一个是和核心的libnx交互,一个编译产出的配置。

Rust FFI

由于现实中很多程序是由不同编程语言写的,必然会涉及到跨语言调用,比如 A 语言写的函数如果想在 B 语言里面调用,这时一般有两种解决方案:一种是将函数做成一个服务,通过进程间通信或网络协议通信;另一种就是直接通过 FFI 调用。前者需要至少两个独立的进程才能实现,而后者直接将其它语言的接口内嵌到本语言中,所以调用效率比前者高。

Rust 本身定位为系统编程语言,不可避免要和现有的 C/C++ 代码交互。

Rust FFI原理和使用文档不少,这里要解决了的主要是简便。因为手写所有绑定是不现实的。

rust-bindgen可以帮助我们生成对应的绑定文件,支持C和部分C++。这里我们先只考虑核心的libnx。

创建build.rs文件

extern crate bindgen;

use std::env;
use std::path::PathBuf;

pub fn main() {
    let devkitpro_path = env::var("DEVKITPRO").unwrap();

    println!("cargo:rustc-link-lib=static=nx");

    println!(
        "cargo:rustc-link-search=native={}/libnx/lib",
        devkitpro_path
    );

    // The bindgen::Builder is the main entry point
    // to bindgen, and lets you build up options for
    // the resulting bindings.
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate bindings for.
        .trust_clang_mangling(false)
        .use_core()
        .ctypes_prefix("lang_items")
        .header("wrapper.h")
        .clang_arg(format!("-I{}/libnx/include", devkitpro_path))
        .clang_arg(format!(
            "-I{}/devkitA64/aarch64-none-elf/include",
            devkitpro_path
        ))
        .bitfield_enum("HidMouseButton")
        .bitfield_enum("HidKeyboardModifier")
        .rustified_enum("HidKeyboardScancode")
        .bitfield_enum("HidControllerType")
        .rustified_enum("HidControllerLayoutType")
        .bitfield_enum("HidControllerColorDescription")
        .bitfield_enum("HidControllerKeys")
        .rustified_enum("HidControllerJoystick")
        .bitfield_enum("HidControllerConnectionState")
        .rustified_enum("HidControllerID")
        .generate_inline_functions(true)
        .blacklist_type("u8")
        .blacklist_type("u16")
        .blacklist_type("u32")
        .blacklist_type("u64")
        .blacklist_type("u128")
        .layout_tests(false)
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    // Write the bindings to the $OUT_DIR/bindings.rs file.
    let out_path = PathBuf::from("src");
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

核心配置就是链接nx库,并指定搜索地址,另外就是include的搜索地址,以后要加新的外部库也需要新增。生成的位置我放在src目录的,主要是为了智能感应,这个文件需要gitignore掉,很多配置是输出到out目录的,本质上没有区别。

新建wrapper.h文件

#include <switch.h>
#include <stdio.h>

在rust代码中直接引用即可

include!("bindings.rs");

构建配置

构建的配置大同小异主要是两点,一是交叉编译,另外一个是switch app本身的配置

交叉变异使用cargo-xbuild,target配置如下

{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "thiscall",
    "win64",
    "sysv64"
  ],
  "arch": "aarch64",
  "data-layout": "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128",
  "executables": true,
  "linker": "aarch64-none-elf-ld",
  "linker-flavor": "ld",
  "linker-is-gnu": true,
  "llvm-target": "aarch64-unknown-none",
  "no-compiler-rt": true,
  "features": "+a53,+strict-align",
  "max-atomic-width": 128,
  "os": "none",
  "panic": "abort",
  "panic-strategy": "abort",
  "position-independent-executables": true,
  "relocation-model": "pic",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "64",
  "disable-redzone": true
}

整个Makefile配置如下,其中部分是应用相关的配置,注意修改。icon是jpg格式,256*256像素。

#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------

ifeq ($(strip $(DEVKITPRO)),)
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
endif

TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules

#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm".
#
# NO_ICON: if set to anything, do not use icon.
# NO_NACP: if set to anything, no .nacp file is generated.
# APP_TITLE is the name of the app stored in the .nacp file (Optional)
# APP_AUTHOR is the author of the app stored in the .nacp file (Optional)
# APP_VERSION is the version of the app stored in the .nacp file (Optional)
# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional)
# ICON is the filename of the icon (.jpg), relative to the project folder.
#   If not set, it attempts to use one of the following (in this order):
#     - <Project name>.jpg
#     - icon.jpg
#     - <libnx folder>/default_icon.jpg
#---------------------------------------------------------------------------------
TARGET		:=	$(notdir $(CURDIR))
OUTDIR		:=	out
BUILD			:=	build
SOURCES		:=	src
DATA			:=	data
INCLUDES	:=	include
EXEFS_SRC	:=	exefs_src

APP_TITLE		:= Switch Backup
APP_AUTHOR	:= htynkn
APP_VERSION	:= 0.0.1

#---------------------------------------------------------------------------------
# rust variables
#---------------------------------------------------------------------------------
TARGET_TRIPLE ?= aarch64-none-elf
XARGO ?= RUST_TARGET_PATH="$(TOPDIR)" cargo xbuild
CARGO ?= RUST_TARGET_PATH="$(TOPDIR)" cargo

RUST_BINARY := $(shell cat $(TOPDIR)/Cargo.toml | grep name | cut -d\" -f 2 | tr - _)
RUST_BUILD_DIR := $(TOPDIR)/target/$(TARGET_TRIPLE)
RUST_RELEASE_LIB := $(RUST_BUILD_DIR)/release/lib$(RUST_BINARY).a
RUST_DEPS = $(TOPDIR)/Cargo.toml $(TOPDIR)/build.rs $(TOPDIR)/src/*.rs
RUST_LIB := $(TOPDIR)/$(BUILD)/lib$(RUST_BINARY).a

#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH	:=	-march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE

CFLAGS	:=	-g -Wall -O2 -ffunction-sections \
			$(ARCH) $(DEFINES)

CFLAGS	+=	$(INCLUDE) -DSWITCH

CXXFLAGS	:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11

ASFLAGS	:=	-g $(ARCH)
LDFLAGS	=	-specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)

LIBS	:= -lnx -L. -l$(RUST_BINARY)

#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib
#---------------------------------------------------------------------------------
LIBDIRS	:= $(PORTLIBS) $(LIBNX)


#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------

export OUTPUT	:=	$(CURDIR)/$(OUTDIR)/$(TARGET)
export TOPDIR	:=	$(CURDIR)

export VPATH	:=	$(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
			$(foreach dir,$(DATA),$(CURDIR)/$(dir))

export DEPSDIR	:=	$(CURDIR)/$(BUILD)

CFILES		:=	$(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES	:=	$(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES		:=	$(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES	:=	$(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))

#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
#---------------------------------------------------------------------------------
	export LD	:=	$(CC)
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
	export LD	:=	$(CXX)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------

export OFILES	:=	$(addsuffix .o,$(BINFILES)) \
			$(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)

export INCLUDE	:=	$(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
			$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
			-I$(CURDIR)/$(BUILD)

export LIBPATHS	:=	$(foreach dir,$(LIBDIRS),-L$(dir)/lib)

export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC)

ifeq ($(strip $(ICON)),)
	icons := $(wildcard *.jpg)
	ifneq (,$(findstring $(TARGET).jpg,$(icons)))
		export APP_ICON := $(TOPDIR)/$(TARGET).jpg
	else
		ifneq (,$(findstring icon.jpg,$(icons)))
			export APP_ICON := $(TOPDIR)/icon.jpg
		endif
	endif
else
	export APP_ICON := $(TOPDIR)/$(ICON)
endif

ifeq ($(strip $(NO_ICON)),)
	export NROFLAGS += --icon=$(APP_ICON)
endif

ifeq ($(strip $(NO_NACP)),)
	export NROFLAGS += --nacp=$(CURDIR)/$(OUTDIR)/$(TARGET).nacp
endif

ifneq ($(APP_TITLEID),)
	export NACPFLAGS += --titleid=$(APP_TITLEID)
endif

.PHONY: $(BUILD) clean all

#---------------------------------------------------------------------------------
all: $(RUST_RELEASE_LIB) $(BUILD)

$(BUILD):
	mkdir -p [email protected] $(BUILD) $(OUTDIR)
	@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile

#---------------------------------------------------------------------------------
clean:
	@echo clean ...
	@$(CARGO) clean
	@rm -fr $(OUTDIR) $(BUILD)/* target src/bindings.rs

#---------------------------------------------------------------------------------
else
.PHONY:	all

DEPENDS	:=	$(OFILES:.o=.d)

#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
all	:	$(OUTPUT).pfs0 $(OUTPUT).nro

$(OUTPUT).pfs0	:	$(OUTPUT).nso

$(OUTPUT).nso	:	$(OUTPUT).elf

ifeq ($(strip $(NO_NACP)),)
$(OUTPUT).nro	:	$(OUTPUT).elf $(OUTPUT).nacp
else
$(OUTPUT).nro	:	$(OUTPUT).elf
endif

$(OUTPUT).elf	:	$(OFILES) $(RUST_LIB)

#---------------------------------------------------------------------------------
# you need a rule like this for each extension you use as binary data
#---------------------------------------------------------------------------------
%.bin.o	:	%.bin
#---------------------------------------------------------------------------------
	@echo $(notdir $<)
	@$(bin2o)

-include $(DEPENDS)

#---------------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------------

#---------------------------------------------------------------------------------
# rust rules
#---------------------------------------------------------------------------------
check:
	@$(XARGO) check --target=$(TARGET_TRIPLE)

$(RUST_RELEASE_LIB): $(RUST_DEPS)
	@echo "+ Building [email protected] [cargo build --release] (via shell out)"
	@echo "+ Command: $(XARGO) --release --target=$(TARGET_TRIPLE)"
	#@$(XARGO) build --release --target=$(TARGET_TRIPLE)
	@$(shell bash -c '$(XARGO) --release --target=$(TARGET_TRIPLE)')

$(RUST_LIB): $(RUST_RELEASE_LIB) | $(BUILD_DIR)
	@cp $< [email protected]

构建效果

switch-build

CI构建

涉及到这种要交叉编译的东西依赖环境都比较麻烦,要安装llvm等,这里我封装了一个docker镜像可以直接使用,安装了核心依赖外还安装了目前所有可用的portlibs。

使用的时候直接

docker run -v $(pwd):/Switch-Backup -w /Switch-Backup htynkn/switch-devenv bash -c "make"

其中的目录改成你需要的即可。

发表评论

电子邮件地址不会被公开。