2024年10月

本博客所有文章除特别声明外,均采用
CC BY-NC-SA 4.0
许可协议。转载请注明来自
唯你

背景

在 Rust 中,使用 println!打印日志时,输出实际上是发送到标准输出(stdout),而 Android Logcat 专门用于处理和显示应用程序的日志信息,此环境下标准输出实现被重新定义。这意味着 Rust 日志输出不会出现在 Logcat 中。

android_logger
直接与 Android 的日志系统集成,确保日志信息可以按预期出现在 Logcat 中。

配置

android_logger 使用如下

注意这里使用的是 android_logger0.11.0 版本,若使用最新版本可能需要做相关 api 调整。

cargo.toml 中增加如下依赖

[dependencies]
log = "0.4"

[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"

同时引入 log 的原因:

  • log 是一个日志记录的抽象库,提供了一套统一的接口,用于记录日志消息(如 info!、warn!、error! 等)。而 android_logger 是 log 针对 android 平台的一种具体实现。
  • 虽然 android_logger 本身依赖 log,你或许以为不必再额外引用 log,但实际上 android_logger 中类似 log::LevelFilter 和 log::Level::Debug 依然依赖 log 库。

初始化

#[cfg(target_os = "android")]
fn init_logging() {
    android_logger::init_once(
        android_logger::Config::default()
            .with_min_level(log::Level::Debug)
            .with_tag("flutter"),
    );
}

注意此处 with_min_level 级别,若为 debug 则打印时最使用必须是 debug 及以上,用 info 基本是无法打印显示在 logcat 中的

使用

rust 文件使用如下


extern crate android_logger;

添加打印

impl PlatformTextureWithProvider for BoxedPixelData {
    fn create_texture(
        engine_handle: i64,
        payload_provider: Arc<dyn PayloadProvider<Self>>,
    ) -> Result<PlatformTexture<BoxedPixelData>> {
        //使用示例
        log::debug!("2222222223 create_texture");
        log::debug!("this is a debug {}", "raynor");

        PlatformTexture::new(engine_handle, Some(payload_provider))
    }
}

连接 android 设备执行 flutter run 后,logcat 日志输出如下:

√ Built build\app\outputs\flutter-apk\app-debug.apk
Installing build\app\outputs\flutter-apk\app-debug.apk...           6.3s
W/FlutterAnimationAdvance( 3237): FlutterAnimationAdvance getInstance()
D/flutter ( 3237): irondash_texture::platform::platform_impl: 2222222223 create_texture
D/flutter ( 3237): irondash_texture::platform::platform_impl: this is a debug raynor

注意

因为每次运行都会重新编译库文件,没必要在 rust 中每次修改日志后执行 cargo clean。 直接运行对于平台支持命令(如 flutter 的话 flutter run)即可看到修改后日志生效输出,。

C语言之声明

1.声明与定义

声明语法

说明符(说明类型或修改缺省属性) 声明表达式列表

  • 说明符
    • 类型说明:int, float
    • 存储属性:static,auto
    • 类型限定:const, volatile

声明 VS 定义

  • 说明类型:取值范围和合法操作
  • 定义:分配存储空间

2.初始化

显式初始化

  • 静态变量(含全局变量):使用
    常量表达式
    初始化
    一次
  • 自动变量:每次执行时使用赋值语句初始化

非显式初始化

  • 静态变量(含全局变量):编译时初始化,缺省值为0
  • 自动变量:运行时初始化,缺省为无效值

3.复杂声明与typedef

复杂声明

左右法则:从最里面的圆括号(未定义的标识符)开始,先看其右边,再看其左边,遇到括号时调转方向。一旦解析完括号的内容即可跳出圆括号,重复该过程直到解析完毕。

int *(*(*f)(int))[10];
// (*f) f是一个指针
// (*f1(int)) f1是一个函数指针,所指向的函数返回值是一个指针,参数是(int)
// int *f2[10] f2是一个数组指针,指向数组 int * a[10]
// 综上,f是一个函数指针,指向函数参数为(int), 返回值为指向 int *a[10]的数组指针,其所指向的数组元素类型为 int*
// pointer to function returning pointer to array[10] of pointer to int

char (*(*x())[])();
// *x(), x是一个函数,返回值类型是一个指针
// *(x1)[], x1是一个指针数组,元素类型是指针
// char x2(), x2是一个函数,返回值类型为char
// 综上,x是一个函数,返回值类型是一个指针数组,其数组元素类型是返回值类型为char的函数的指针
// function returning pointer to array[] of pointer to function returning char

char (*(*x[3])())[5];
// *x[3], x是一个数组,元素类型是指针
// *x1(), x1是一个函数,返回值是指针
// char x2[5], x2是数组,元素类型是char
// 综上,x是一个数组,元素类型是函数指针,该函数返回值类型是数组指针,指向元素类型为char的数组
// array[3] of pointer to function returning pointer to array[5] of char

Unix 系统的
cdecl
程序实现了声明的解析,可参考 comp.sources.unix.newsgroup

typedef

语法和声明类似,将标识符作为类型的别名

1. 让代码更加清晰简洁

  • 定义结构体,联合,枚举等变量

    typedef struct student {
      char name[];
      int score;
    } T_Stu, *PT_Stu;
    
    T_Stu tStu1 = {"Bob", 78};
    PT_Stu ptStu1 = &tStu1;
    
    typedef enum color {
      red, white, block,
    } colot_t;
    color_t color1 = red;
    
  • 简化复杂声明

    // int *(*array[10])(int *p);
    typedef int *(func_ptr)(int *p);
    func_prt array[10];
    

2. 增加代码的可移植性

int 类型在不同的编译器和平台下所分配的存储字节不同,使用自定义的数据类型而不是内置类型来增强可移植性

#ifdef PIC_16
typedef unsigned long u32  // 2 bytes for int, 4 bytes for long
#else
typedef unsigned int u32  // 4 bytes for both int and long in PIC_32
#endif

typedef 的适用情景

  • 创建一个数据类型的别名
  • 跨平台的指定长度的类型, u32
  • 与操作系统,BSP,网络字节相关的数据类型,如size_t, pid_t等
  • 不透明的数据类型,需要隐藏数据结构实现细节,只开放函数接口

避免滥用typedef: 参考 Linux Kernel Documennt的CodingStyle

4.辨析

  • 数组与指针参数

    int fun(char *str);
    int func(char str[]);
    // 二者仅在当前声明上下文一致
    
  • 数组和指针初始化

    char *p = "hello";
    char a[] = "hello"; // 特殊形式,等价于 char a[] = {'h', 'e', 'l', 'l', 'o', '\0'};
    
  • 声明与定义

    int a;            // 变量的声明和定义
    extern int b;     // 变量的声明
    void f1(void){};  // 函数的声明和定义
    void f2(void);    // 函数的声明
    
  • typedef VS #define

    typedef等价于存储类关键字,宏定义只是字符串替换


    • typedef 不支持继续使用static等存储类关键字
    • 宏定义不支持指针声明
    • typedef 具有作用域,宏定义在预处理阶段进行全局替换

    #define int *POINTER_TO_INT
    POINTER_TO_INT a, b, c; // b, c 无法被声明为指针类型
    
    // 与const关键字
    typedef char* PTCHAR1;
    #define PTCHAR2 char*;
    
    const PTCHAR1 p1; // PTCHAR1作为类型,可与consat调换位置,const修饰p1,指针常量
    const PTCHAR2 p2; // 等价于 const char* p2; char可与const调换位置,const修饰(*p2),常量指针
    

//TODO Linux 内核中的声明,学习Linux源码时

5.参考

  • 《C和指针》
    • 3.2 声明
    • 3.3 typedef
    • 13.2 高级声明
  • 《嵌入式C语言的自我修养》
    • 7.5 typedef
    • 7.8.2 复杂声明
    • 9.4 头文件的深度解析
  • 《C程序设计语言》
    • 2.4 声明
    • 4.9 初始化
    • 5.12 复杂声明
    • 6.7 类型定义(typedef)
  • 《C专家编程》
    • 3 C语言的声明
    • 4.3 声明与定义

SDDC Manager 中有两种类型的软件包,分别是“升级/修补包(PATCH)”和“安装包(INSTALL)”。“升级/修补包”用于执行 VCF 环境中组件的升级/修补,这个已经在前面的文章中使用过了;而另外一种“安装包”,这种包用于在 VCF 环境中部署其他集成解决方案,比如 VMware Aria Suite Lifecycle Manager 以及 VMware Avi Load Balancer 等,当然还有一些组件安装包可以用来代替源物料清单(BOM)版本中的软件包以进行异步部署 VI 工作负载域。

导航到 SDDC Manager UI->生命周期管理->包管理,当这些软件包下载之后,可以在“下载历史记录”中找到它。但是,你可能会发现,这些已下载的包没有选项可以对其执行删除操作。

默认情况下,SDDC Manager 所下载的软件包会存放到虚拟机的 /nfs/vmware/vcf/nfs-mount 目录,如下图所示。这个目录的空间是有限的,随着下载的软件包越来越多,可用空间会越来越少直至被完全占满,因此,我们需要对这些软件包进行管理。

VMware 专门提供了一个 PowerShell 脚本用来管理 SDDC Manager 中的软件包,比如查看包信息、删除、下载软件包等。你可以在知识库文章
KB 313523
底部找到这个脚本,查看并了解相关说明后,下面一起来看看如何使用它。

一、环境要求

使用这个 PowerShell 脚本需要安装一些运行环境,因为脚本所执行的相关操作需要调用这些环境中的命令或 API 才能完成,具体如下。可以参考这篇(
使用 PowerVCF 连接和管理 VMware Cloud Foundation 环境。
)文章中方法准备这些环境。

  • 运行 PowerShell Core 7.3.0 或更高版本。
  • 运行 PowerCLI 13.1 或更高版本。
  • 运行 PowerVCF 2.3.0 或更高版本。

二、连接 SDDC Manager

下载脚本到本地后,打开 PowerShell Core 并进入到脚本所在的目录,运行以下命令连接到 SDDC Manager。也可以不带参数直接运行脚本,系统会提示并要求你输入所需的参数信息。

.\VcfBundleManagement.ps1 -server <SDDC_Manager_FQDN> -user <SDDC_Manager_SSO_User> -pass <SDDC_Manager_SSO_Password> -rootPass <SDDC_Manager_Root_Password>

参数说明:

  • <SDDC_Manager_FQDN>是 SDDC Manager 的管理地址。
  • <SDDC_Manager_SSO_User> 是 SDDC Manager 的 SSO 管理员用户名。
  • <SDDC_Manager_SSO_Password>是 SDDC Manager 的 SSO 管理员密码。
  • <SDDC_Manager_Root_Password>是 SDDC Manager 的 Root 用户密码。

三、使用 PowerShell 脚本

连接成功后,将出现如下图所示的选项菜单。这个脚本所提供的菜单选项可以实现不同的功能,输入不同的“数字”用于执行不同的任务,输入“Q”退出 PowerShell 脚本。下面来看看,这些不同的选项分别可以执行哪些操作。

输入数字“4”并回车,用于查看 SDDC Manager 中软件包存放目录的磁盘空间使用情况。注,可以按“回车”回到选项菜单。

输入数字“1”并回车,用于刷新 SDDC Manager 中软件包的清单信息。

输入数字“2”并回车,用于查看 SDDC Manager 中所有的软件包清单。

输入数字“3”并回车,用于查看 SDDC Manager 中所有已下载的软件包。

输入数字“5”并回车,用于删除 SDDC Manager 中指定的软件包。若删除多个,需要用逗号进行分开。注,输入“B”可以回到选项菜单。

输入数字“3”并回车,再次查看 SDDC Manager 中所有已下载的软件包。注,如果软件包显示还在,可以输入数字“1”重新进行刷新。

输入数字“6”并回车,用于删除 SDDC Manager 中指定版本的所有软件包。

输入数字“3”并回车,再次查看 SDDC Manager 中所有已下载的软件包。

输入数字“8”并回车,用于下载 SDDC Manager 中指定的软件包。若下载多个,需要用逗号进行分开。注,访问
KB 96099
了解有关软件包的更多信息。

输入数字“3”并回车,再次查看 SDDC Manager 中所有已下载的软件包。

输入数字“7”并回车,用于删除 SDDC Manager 中所有已过时的软件包。当前工作负载域版本是 5.2.0.0,所以 5.1.1.0 版本相对来说是过时软件包。

如果没有低于 5.2.0.0 的软件包,则会出现如下图所示的警告。

输入数字“9”并回车,用于下载 SDDC Manager 中指定版本的所有软件包。注,如果软件包已经下载,则清单不会列出。

输入数字“3”并回车,再次查看 SDDC Manager 中所有已下载的软件包。注,任务已取消,如果下载成功则应显示“SUCCESSFUL”状态。

一:背景

1. 讲故事

在 .NET AOT 编程中,难免会在 泛型,序列化,以及反射的问题上纠结和反复纠错尝试,这篇我们就来好好聊一聊相关的处理方案。

二:常见问题解决

1. 泛型问题

研究过泛型的朋友应该都知道,从
开放类型
上产下来的
封闭类型
往往会有单独的 MethodTable,并共用 EEClass,对于值类型的泛型相当于是不同的个体,如果在 AOT Compiler 的过程中没有单独产生这样的个体信息,自然在运行时就会报错,这么说可能有点懵,举一个简单的例子。


    internal class Program
    {
        static void Main(string[] args)
        {
            var type = Type.GetType(Console.ReadLine());

            try
            {
                var mylist = typeof(List<>).MakeGenericType(type);

                var instance = Activator.CreateInstance(mylist);
                int count = (int)mylist.GetProperty("Count").GetValue(instance);
                Console.WriteLine(count);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                Console.WriteLine(ex.Message);
            }
            Console.ReadLine();
        }
    }

    public class Location
    {
    }

从上图看直接抛了一个异常,主要原因在于
Location
被踢出了依赖图,那怎么办呢?很显然可以直接
new List<Location>
到依赖图中,但在代码中直接new是非常具有侵入性的操作,那如何让侵入性更小呢?自然就是借助 AOT 独有的 rd (Runtime Directives) 这种xml机制,具体可参见:
https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/rd-xml-format.md

rd机制非常强大,大概如下:

1)可以指定程序集,类型,方法作为编译图的根节点使用,和 ILLink 有部分融合。
2)可以手工的进行泛型初始化,也可以将泛型下的某方法作为根节点使用。
3)为Marshal和Delegate提供Pinvoke支持。

在 ilc 源码中是用 compilationRoots 来承载rd过去的根节点,可以一探究竟。


foreach (var rdXmlFilePath in Get(_command.RdXmlFilePaths))
{
    compilationRoots.Add(new RdXmlRootProvider(typeSystemContext, rdXmlFilePath));
}

有了这些知识就可以在 rd.xml 中实例化
List<Location>
了,参考如下:


<?xml version="1.0" encoding="utf-8" ?>
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
	<Application>
		<Assembly Name="Example_21_1">
			<Type Name="System.Collections.Generic.List`1[[Example_21_1.Location,Example_21_1]]" Dynamic="Required All" />
		</Assembly>
	</Application>
</Directives>

同时在 csproj 做一下引入即可。


<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<PublishAot>true</PublishAot>
		<InvariantGlobalization>true</InvariantGlobalization>
	</PropertyGroup>
	<ItemGroup>
		<RdXmlFile Include="rd.xml" />
	</ItemGroup>
</Project>

执行之后如下,要注意一点的是
Dynamic="Required All"
它可以把
List<Location>
下的所有方法和字段都注入到了依赖图中,比如下图中的 Count 属性方法。

2. 序列化问题

序列化会涉及到大量的反射,而反射又需要得到大量的元数据支持,所以很多第三方的Json序列化无法实现,不过官方提供的Json序列化借助于 SourceGenerator 将原来 dll 中的元数据迁移到了硬编码中,从而变相的实现了AOT的Json序列化,参考代码如下:


namespace Example_21_1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var person = new Person()
            {
                Name = "john",
                Age = 30,
                BirthDate = new DateTime(1993, 5, 15),
                Gender = "Mail"
            };

            var jsonString = JsonSerializer.Serialize(person,
                                            SourceGenerationContext.Default.Person);

            Console.WriteLine(jsonString);
            Console.ReadLine();
        }
    }
}

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Person))]
internal partial class SourceGenerationContext : JsonSerializerContext { }

public class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }
    public string Gender { get; set; }
}

当用 VS 调试的时候,你会发现多了一个
SourceGenerationContext.Person.g.cs
文件,并且用
properties
数组承载了 Person 的元数据,截图如下:

3. 反射问题

反射其实也是一个比较纠结的问题,简单的反射AOT编译器能够轻松推测,但稍微需要上下文关联的就搞不定了,毕竟涉及到上下文关联需要大量的算力,而目前的AOT编译本身就比较慢了,所以暂时没有做支持,相信后续的版本会有所改进吧,接下来举一个例子演示下。


    internal class Program
    {
        static void Main(string[] args)
        {
            Invoke(typeof(Person));

            Console.ReadLine();
        }

        static void Invoke(Type type)
        {
            var props = type.GetProperties();

            foreach (var prop in props)
            {
                Console.WriteLine(prop);
            }
        }
    }

    public class Person
    {
        public int Age { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        public string Gender { get; set; }
    }

这段代码在 AOT中是提取不出属性的,因为
Invoke(typeof(Person));

type.GetProperties
之间隔了一个
Type type
参数,虽然我们肉眼能知道这个代码的意图,但 ilc 的深度优先它不知道你需要 Person中的什么,所以它只保留了 Person 本身,如果你想直面观测的话,可以这样做:


  1. <PublishAot>true</PublishAot>
    改成
    <PublishTrimmed>true</PublishTrimmed>
  2. 使用 dotnet publish 发布。
  3. 使用ILSPY观测。

截图如下,可以看到 Person 空空如也。

有了这个底子就比较简单了,为了让 Person 保留属性,可以傻乎乎的用
DynamicallyAccessedMembers
来告诉AOT我到底想要什么,比如
PublicProperties
就是所有的属性,当然也可以设置为 ALL。


        static void Invoke([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
        {
            var props = type.GetProperties();

            foreach (var prop in props)
            {
                Console.WriteLine(prop);
            }
        }

如果要想侵入性更小的话,可以使用 TrimmerRootDescriptor 这种外来的 xml 进行更高级别的定制,比如我不想要
Gender
字段 ,具体参考官方链接:
https://github.com/dotnet/runtime/blob/main/docs/tools/illink/data-formats.md#xml-examples


<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<PublishAot>true</PublishAot>
		<InvariantGlobalization>true</InvariantGlobalization>
		<IlcGenerateMapFile>true</IlcGenerateMapFile>
	</PropertyGroup>
	<ItemGroup>
		<TrimmerRootDescriptor Include="link.xml" />
	</ItemGroup>
</Project>

然后就是 xml 配置。


<?xml version="1.0" encoding="utf-8" ?>
<linker>
	<assembly fullname="Example_21_1">
		<type fullname="Example_21_1.Person">
			<property signature="System.Int32 Age" />
			<property signature="System.String Name" />
			<property signature="System.DateTime BirthDate" />
		</type>
	</assembly>
</linker>

从下图看,一切都是那么完美。

三:总结

在将程序发布成AOT的过程中,总会遇到这样或者那样的坑,这篇算是提供点理论基础给后来者吧,同时 Runtime Directives 这种无侵入的实例化方式,很值得关注哈。

图片名称

ijkplayer是一款由B站研发的移动端国产播放器,它基于FFmpeg3.4版本,同时兼容Android和iOS两大移动操作系统。ijkplayer的源码托管地址为https://github.com/bilibili/ijkplayer,截止2024年9月15日,ijkplayer获得3.24万星标数,以及0.81万个分支数,而这还是ijkplayer停止更新6年之后的数据,可想而知当年的ijkplayer是多么火爆。

不过正因为ijkplayer多年未更新,按照导包方式仅能在较老的平台上编译运行,比如ijkplayer支持的Android平台仅限于API 9~23,支持的iOS平台仅限于iOS 7.0~10.2.x。为了让ijkplayer能够在更新的开发环境上正常运行,需要先在Linux系统上交叉编译ijkplayer在Android平台上的so文件,才能在App工程中导入并调用so库。下面介绍如何在Linux编译ijkplayer的so库。

一、准备Linux编译环境

首先在Linux系统执行下面命令安装编译工具。

yum install git make yasm

接着执行下面命令临时调整tmp分区大小,确保系统的临时空间充足,避免解压大文件失败。

mount -o remount,size=2G /tmp

二、安装Android的SDK和NDK

依次执行下列命令下载并安装Android的SDK,注意不要用太高版本的SDK,因为ijkplayer没有适配高版本的SDK。

mkdir -p /usr/local/src_ijkplayer
cd /usr/local/src_ijkplayer
curl -O https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
unzip sdk-tools-linux-4333796.zip
mkdir sdk
mv tools sdk/cmd_tools
cd sdk/cmd_tools/bin
./sdkmanager "build-tools;28.0.3" "platforms;android-28"

依次执行下列命令下载并安装Android的NDK,注意不要用太高版本的NDK,因为ijkplayer没有适配高版本的NDK,官方推荐采用r10e版本的NDK即可。

cd /usr/local/src_ijkplayer
curl -O https://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip
unzip android-ndk-r10e-linux-x86_64.zip

执行下面的环境变量设置命令,分别设置SDK的环境变量ANDROID_SDK,以及NDK的环境变量ANDROID_NDK。

export ANDROID_SDK=/usr/local/src_ijkplayer/sdk
export ANDROID_NDK=/usr/local/src_ijkplayer/android-ndk-r10e

三、下载并编译ijkplayer

先执行以下命令下载ijkplayer的源码包。

cd /usr/local/src_ijkplayer
git clone https://github.com/Bilibili/ijkplayer.git

再执行以下命令检查并初始化ijkplayer的Android编译环境。

cd ijkplayer
./init-android-openssl.sh
./init-android.sh

然后依次执行下列命令,分别编译ijkplayer需要的openssl库和ffmpeg库,以及ijkplayer的so库。之所以在三个脚本后面添加“ arm64”,是为了只编译适配arm64指令的so文件。

cd android/contrib
./compile-openssl.sh arm64
./compile-ffmpeg.sh arm64
cd ../
./compile-ijk.sh arm64

一切顺利的话,即可在ijkplayer/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a目录下看到编译好的三个so库:libijkffmpeg.so、libijkplayer.so、libijksdl.so。把包含三个so文件在内的整个libs目录复制到App工程的libs目录,即可完整ijkplayer的so库导入工作。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。