在 Foobar2000 中直接播放网易云音乐的缓存文件

又是一个奇怪的需求:用 Foobar2000 播放网易云音乐缓存的文件。

昨天晚上本来是计划做乐曲改编的。虽然源用的是网易云音乐,但是播放器我就不想用它,而是 Foobar2000。但是当我尝试下载的时候,却被提示仅允许付费/会员下载。

行吧,你是哪天买的版权(如果真买了的话,笑)我不知道;但是反正我没有很高的码率要求,默认播放的 128 Kbps 就行;而且如果这也不行,还有别的手段。这时候我就想到,虽然不可以下载,但是还是可以播放的啊。既然能播放,那么应该就缓存在本地了,除非产品设计抽风。

于是我就看了看缓存目录。结果发现了很多大小接近 128 Kbps MP3 的文件。(是不是有一种既视感?)主体内容应该是音频没错,但是经过了某种形式的加密。不过这次不是 CTF 了,所以就直接 Google 吧。已经有人搞定了。果然还是屈服于性能,只采用了简单的异或。哈。

那么现在的目标就是把这个逻辑封装为一个 Foobar2000 的组件,这样就可以直接在 Foobar2000 中播放了。虽然我写过简单的组件,不过这次不一样,用到的东西比 input_stub 要多一些。

这次稍微触及到了 Foobar2000 组件系统的核心之一,service_base。工程的目标是:实现一个文件流,对原文件流做一个读写封装。其实这次还算简单,是无状态的,简单异或每个字节就行了。但是麻烦在于 service_base 的设计。如果在 .NET 里面实现这个可是太简单了,继承并重写,重写中调用父类方法就行。service_base 是引用计数的,有点像 COM(接口查找和接口升级更像),但是(在 release 模式下)并没有虚表(V-table),成员查找完全依赖于模板。成员一般不使用原生指针,而是使用 service_ptr_t<T>。我本来就没做过 COM 开发,而且这玩意儿还是没有虚表的。

根据 SDK 附带的简单说明,继承自 service_base 的“类”实际上是接口(类似 IUnknown),不应该有任何字段和方法实现,实现细节相关应该在在对应的 *_impl 类中提供。等等,那我该怎么实例化我的 service 呢?我的指针模板应该填什么类型呢?怎么转换接口呢?

看一下相关的定义。如果你知道 COM 的话这个看起来绝对不陌生。

class NOVTABLE service_base
{
public:
    virtual int service_release() throw() = 0;
    virtual int service_add_ref() throw() = 0;
    virtual bool service_query(service_ptr & p_out,const GUID & p_guid) = 0;
    // ...
}

class NOVTABLE file : public service_base, public stream_reader, public stream_writer
{
    // ...
}

但是要注意的是那些 NOVTABLE 修饰,这是和 COM 最大的不同。在 VC++ 的 release 模式下,它会被扩展为 __declspec(novtable)这样尽管在逻辑上,file 继承自 service_base,但是在编译后它们并没有 V-table,因此不可以进行虚函数调用,dynamic_cast 也应该会失效。关于 __declspec(novtable) 对虚表和 vptr(虚指针)的影响可以参考这个回答。简单说,就是修饰 __declspec(novtable) 没关系,RTTI 和虚函数照常工作(除了在构造/析构函数中)。所以,直观的结论是,要让代码正常工作的话,抽象类(也就是 Foobar2000 中的接口)可以修饰,但是抽象类的非抽象子类不可以;抽象类自身也不能在构造/析构函数中使用虚表或虚指针相关。

怎么实例化呢?一种方法是使用工厂类(service_factory_base),不过过程就非常麻烦了。对于简单的接口,SDK 提供了一个非常方便的函数 fb2k::service_new()。只需要创建实现的实例,赋值给接口指针就行了。

在代码中,我定义了一个自定义文件流类型和它的实现:

class NOVTABLE mapped_file : public file
{
    // ...
}

// 注意这个 NOVTABLE,可以这么写是因为 mapped_file_impl_t 还是抽象的,实例化由 service_impl_t 负责,见下文
class NOVTABLE mapped_file_impl_t : public mapped_file
{
    // ...
}

在使用上就可以这样:

service_ptr_t<mapped_file> ptr = fb2k::service_new<mapped_file_impl_t>(...);
// 或者
service_ptr_t<file> ptr = fb2k::service_new<mapped_file_impl_t>(...);

有趣的是,service_new() 内部使用了 service_impl_t<T>。这个类型继承自 implement_service_query<T>,而正是 implement_service_query<T> 提供了新的 service_query() 的默认实现。service_impl_t<T> 则进一步提供了 service_add_ref()service_release() 的默认实现。所以在写自己的类型实现(比如 mapped_file_impl_t)时,是不需要提供这三个方法的。因此,此时这个“类型实现”对于编译器而言还是抽象类型,不可以直接使用 new 实例化——将所有实例的创建过程进行托管,可以有效地消除某些 bug——这是一个附加的好处。service_impl_t<T> 正好可以作为使用模板来创建 mixin 的一个例子。

指针模板应该填什么类型呢?由于巧妙的向上查找方式(见下文),所以可以使用实现类型或者其任意的父类型。

怎么转换接口呢?其实这个组件的功能并没有涉及到这个问题,但是在开始摸索的时候,我也是实现了整个 mapped_file,所以大略知道一点。每个继承自 service_base 的类都要提供一个静态成员 GUID class_guid(用于特征萃取),在 service_query()(1.4 以后是 handle_service_query())中进行判断。查询整体就是一个沿着继承链往上找的过程。思想则是,一个子类声明它能被转换为哪些父类。而这个继承链,实际上是编译期的,利用模板而不是虚表。可以看 handle_service_query() 的实现:

static bool service_query_walk(service_ptr &, const GUID &, service_base *) {
    return false;
}

template<typename interface_t> static bool service_query_walk(service_ptr & out, const GUID & guid, interface_t * in) {
    if (guid == interface_t::class_guid) {
        out = in; return true;
    }
    typename interface_t::t_interface_parent * chain = in;
    return service_query_walk(out, guid, chain);
}

template<typename class_t> static bool handle_service_query(service_ptr & out, const GUID & guid, class_t * in) {
    typename class_t::t_interface * in2 = in;
    return service_query_walk( out, guid, in2 );
}

其中 t_interfacet_interface_parent 在接口的 FB2K_MAKE_SERVICE_INTERFACE 中被自动定义。其实就是宏的那两个参数。这里实现了自动沿着继承链向上查找的语义,终止则利用是模板参数的退化(不是无参特化,但是我不确定该怎么称呼)用一个重载实现的。

看来 Peter 也是把 C++ 吃透了。这些 API 的设计非常巧妙。

最终的成果是个小玩意儿。使用 VS 2019 编译。

分享到 评论