内核中的封装继承与多态

发布于 2021-03-26 21:38:14

内核中的封装继承与多态

RT-Thread 虽然是使用面向过程的 C 语言来编写,但是处处都体现了面向对象的编程思想,先前对其感悟不够深,随着编写的程序越来愈多,对其理解也逐步加深。

封装

封装是一种信息隐蔽技术,它体现于类的说明,是对象的重要特性。封装使数据和加工该数据的方法(函数)封装为一个整体,以实现独立性很强的模块,使得用户只能见到对象的外特性(对象能接受哪些消息,具有那些处理能力),而对象的内特性(保存内部状态的私有数据和实现加工能力的算法)对用户是隐蔽的。封装的目的在于把对象的设计者和对象者的使用分开,使用者不必知晓行为实现的细节,只须用设计者提供的消息来访问该对象。

在C语言中,大多数函数的命名方式是动词+名词的形式,例如要获取一个 semaphore,会命名成take_semaphore,重点在take这个动作上。在RT-Thread系统的面向对象编程中刚好相反,命名为 rt_sem_take,即名词+动词的形式,重点在名词上,体现了一个对象的方法。另外对于某些方法,仅局限在对象内部使用,它们将采用static修辞把作用范围局限在一个文件的内部。通过这样的方式,把一些不想让用户知道的信息屏蔽在封装里,用户只看到了外层的接口,从而形成了面向对象中的最基本的对象封装实现。

下面给出一段示例代码,展示C语言如何实现封装:

// shape.h
typedef struct{
    int x;
    int y;
}Shape;

Shape * Shape_create(int x, int y);
void Shape_init(Shape * self, int x, int y);
void Shape_move(Shape * self, int dx, int dy);

// shape.c
Shape * Shape_create(int x, int y)
{
    Shape * s = malloc(sizeof(Shape));
    s->x = x;
    s->y = y;
    return s;
}

void Shape_init(Shape * self, int x, int y)
{
    self->x = x;
    self->y = y;
}

void Shape_move(Shape * self, int dx, int dy)
{
    self->x += dx;
    self->y += dy;
}

// main.c
#include "shape.h"
int main(int argc, char *argv[])
{
    Shape * s = Shape_create(0, 0);
    Shape_move(s, 10, 10);
    return 0;
}

这里定义了一个叫做 Shape 的结构体,外界只能通过相关的函数来对这个 Shape 进行操作,例如创建(Shape_create), 初始化(Shape_init), 移动(Shape_move)等,不能直接访问 Shape 的内部数据结构。

如果想隐藏某个方法,即变成私有方法private,只需要在shape.c源文件中相应的方法前加上static限制该函数的作用范围为本文件内就可以了,既然隐藏了该方法也就不必在shape.h中声明该函数了。

虽然这里没有 class 这样的关键字,数据结构和相关操作是分开写的,看起来不太完美, 但确实是实现了封装。

20190923195151524.png

继承

所有的内核对象都继承自更基础的对象,一直到最基础的对象 rt_object,下面从最上层的对象触发,看看这个继承过程是怎样的,以我最近编写的 uart16550 驱动为例。

  • struct zynq_uart16550
struct zynq_uart16550
{
    struct rt_serial_device serial;  // 继承自 rt_serial_device

    XUartNs550 UartNs550Instance;
    struct rt_semaphore rx_sem;
    struct zynq_uart16550_config *config;
};
  • struct rt_serial_device
struct rt_serial_device
{
    struct rt_device          parent;       // 继承自 rt_device

    const struct rt_uart_ops *ops;
    struct serial_configure   config;

    void *serial_rx;
    void *serial_tx;
};
  • struct rt_device
struct rt_device
{
    struct rt_object          parent;        // 继承自 rt_object

    enum rt_device_class_type type;          /**< device type */
    rt_uint16_t               flag;          /**< device flag */
    rt_uint16_t               open_flag;     /**< device open flag */

    rt_uint8_t                ref_count;     /**< reference count */
    rt_uint8_t                device_id;     /**< 0 - 255 */

    /* device call back */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

#ifdef RT_USING_DEVICE_OPS
    const struct rt_device_ops *ops;
#else
    /* common device interface */
    rt_err_t  (*init)   (rt_device_t dev);
    rt_err_t  (*open)   (rt_device_t dev, rt_uint16_t oflag);
    rt_err_t  (*close)  (rt_device_t dev);
    rt_size_t (*read)   (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
    rt_size_t (*write)  (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
    rt_err_t  (*control)(rt_device_t dev, int cmd, void *args);
#endif

#if defined(RT_USING_POSIX)
    const struct dfs_file_ops *fops;
    struct rt_wqueue wait_queue;
#endif

    void                     *user_data;                /**< device private data */
};
  • struct rt_object
struct rt_object                     // 作为基础内核对象
{
    char       name[RT_NAME_MAX];    /**< name of kernel object */
    rt_uint8_t type;                 /**< type of kernel object */
    rt_uint8_t flag;                 /**< flag of kernel object */

#ifdef RT_USING_MODULE
    void      *module_id;            /**< id of application module */
#endif
    rt_list_t  list;                 /**< list node of kernel object */
};

整个继承过程从后到前流程如下:

  1. struct zynq_uart16550
  2. struct rt_serial_device
  3. struct rt_device
  4. struct rt_object

可以看出,每一次继承,更基础的内核类都作为更高级的内核类的第一个成员出现,这就像是每一次继承发生时,都为新产生的子类添加了更多功能,同时也继承了父类的属性。

多态—改写对象行为

在 RT-Thread 中如何体现了多态呢,这一点有时候会带给我困惑,但是如果仔细阅读平时编写的驱动程序的话,只要使用了 RT-Thread 的驱动框架,就会发现多态的实现几乎无处不在。

多态表示不同的对象可以执行相同的动作,但是要通过他们自己实现的代码来执行。什么意思呢,就是以设备举例,所有的 rt_device 都支持打开关闭,读写配置等操作,但是对不同类型设备这些操作的细节是不同的,例如串口设备的读写操作和块设备的读写操作不一样,但归根结底都是读写操作,在这一点上是没有区别的。

再举一个例子,同样是串口设备,这些串口设备都支持读写操作,但是读写 stm32 串口与操作 zynq 串口的细节就是不一样的,但归根结底,都是读写操作。也就是说,在这种情况下,有了父类,也就是串口设备,就知道说要将来可以进行读写操作,但是对于不同的子类来说,这些读写操作的具体细节要由子类来提供,将来真正要读写的时候,就调用子类提供的代码来实际操作。

下面以实际代码来说明多态在 RT-Thread 中的具体实现,首先观察如下代码:

  • struct rt_device
struct rt_device
{
    struct rt_object          parent;        // 继承自 rt_object

    enum rt_device_class_type type;          /**< device type */
    rt_uint16_t               flag;          /**< device flag */
    rt_uint16_t               open_flag;     /**< device open flag */

    rt_uint8_t                ref_count;     /**< reference count */
    rt_uint8_t                device_id;     /**< 0 - 255 */

    /* device call back */
    rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
    rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);

    const struct rt_device_ops *ops;         // 注意,这里向子类提供了 rt_device 类的操作表,子类就可以进行方法重写
                                             // 使用这种方式实现了多态,使得继承自设备父类的子类可以用于操作不同类型的设备
    // 省略多余代码... 

    void                     *user_data;                /**< device private data */
};
  • struct rt_serial_device
struct rt_serial_device
{
    struct rt_device          parent;       // serial device class 继承了父类 rt_device,也就拥有了父类的 rt_device_ops 函数表

    const struct rt_uart_ops *ops;          // 注意这里,这里向子类提供了 rt_uart_ops 类的操作表,子类就可以进行方法重写
    struct serial_configure   config;       // 使用这种方式实现了多态,使得继承自串口父类的子类可以用于操作不同类型的串口设备

    void *serial_rx;
    void *serial_tx;
};
  • struct zynq_uart16550
struct zynq_uart16550
{
    struct rt_serial_device serial;         // zynq_uart1655 设备类继承了父类 rt_serial_device,也就拥有了父类的 rt_uart_ops 函数表

    XUartNs550 UartNs550Instance;
    struct rt_semaphore rx_sem;
    struct zynq_uart16550_config *config;
};
  • struct rt_uart_ops
struct rt_uart_ops                          // 支持某一特定类型串口设备所需要重写的方法
{
    rt_err_t (*configure)(struct rt_serial_device *serial, struct serial_configure *cfg);
    rt_err_t (*control)(struct rt_serial_device *serial, int cmd, void *arg);

    int (*putc)(struct rt_serial_device *serial, char c);
    int (*getc)(struct rt_serial_device *serial);

    rt_size_t (*dma_transmit)(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction);
};

具体实现实现添加操作函数时,实现如下:

static const struct rt_uart_ops zynq_uart16550_ops =  // 为 ops 函数表设置特定串口设备的操作函数
{
    .configure = zynq_uart16550_configure,
    .control = zynq_uart16550_control,
    .putc = zynq_uart16550_putc,
    .getc = zynq_uart16550_getc,
};

接下来使用驱动的初始化代码进行方法重写:

uart16550_obj.serial.ops = &zynq_uart16550_ops;  // 挂接 rt_serial_device 类中的 rt_uart_ops 函数表,相当于重写了函数表中的方法

参考

0 条评论

发布
问题