rask/libload

一个小型FFI库加载工具

0.2.1 2020-02-17 22:56 UTC

This package is auto-updated.

Last update: 2024-09-18 09:04:09 UTC


README

Latest Stable Version Infection MSI

这是一个小型库,可以帮助以直接的方式加载FFI库。

原理

通过PHP FFI加载动态库的常规方法是简单易懂的

  • \FFI::cdef() 允许您接收一个库,并动态编写库头文件定义。
  • \FFI::load() 允许您接收一个库头文件,其中包含指向要加载的动态库的 FFI_LIB

这一切都很好,除了一个问题

您被严格锁定在 dlopen(3) 的逻辑中,这是一个用于加载动态库的C函数。本质上这意味着以下内容

  1. 您不能为 FFI_LIB 使用相对路径,因为这些路径基于当前工作目录,它可以是什么都行
  2. 您不能使用绝对路径,因为所有软件都可以在任何系统上的任何路径上运行
  3. 您不能依赖 LD_LIBRARY_PATH,因为它无法在运行时更改,并且您需要要求代码的用户正确设置它
  4. 您不能使用 /lib/usr/lib,因为这会在用户系统中造成无意义的污染,更不用说需要管理员权限了

在针对有限受众的小项目中,这些限制可能无关紧要。或者,您打算使用的库是一个众所周知且经常预装的库(例如,libc)。但一旦您想分发依赖于自定义构建的FFI库的公共代码,您就麻烦了。

因此,这个库存在的唯一真正原因是绕过这些限制,并允许您以几种不同的方式加载动态库。

特性

  • dlopen(3) 方式加载库
  • 相对于头文件加载库
  • 相对于自定义路径加载库
  • 在目录树中搜索库

示例

<?php

use rask\Libload\Loader;
use rask\Libload\LoaderException;
use rask\Libload\Parsing\ParseException;

$loader = new Loader();

try {
    $ffi = $loader->relativeTo('/path/to/libs')->load(__DIR__ . '/libs/my_lib.h');
} catch (LoaderException | ParseException $e) {
    // log it or something
}

assert($ffi instanceof \FFI);

其中 my_lib.h 包含

#define FFI_LIB "my_lib.so"

... definitions here ...

上面的示例创建了一个新的 Loader 实例,然后我们指示它以相对查找模式加载头文件。

所以,如果动态库存在于 /path/to/libs/my_lib.so,它应该被加载,并为我们返回一个普通的PHP \FFI 实例。

工作原理

简而言之

加载器读取您的头文件,解析 FFI_LIB 定义,然后仅使用 \FFI::cdef() 与头文件以及要实际加载的库的正确路径。

有点巧妙。

安装

$ composer require rask/libload

用法

使用PHP默认逻辑

要使用默认的 \FFI::load() 逻辑(即 dlopen(3) 逻辑),您可以使用加载器如下

<?php

// ... setup autoloads etc ...

$loader = new \rask\Libload\Loader();

$ffi = $loader->load('./path/to/header.h');

assert($ffi instanceof \FFI);

以相对于头文件的方式加载库

如果您的头文件定义了一个位于头文件本身的 FFI_LIB,您可以使用

<?php

// ... setup autoloads etc ...

$loader = new \rask\Libload\Loader();

$ffi = $loader->relativeToHeader()->load('./path/to/header.h');

assert($ffi instanceof \FFI);

您的头文件可能包含

#define FFI_LIB "./subdir/lib.so"

所以 $loader 会查找库在 ./path/to/header/subdir/lib.so

如果 FFI_LIB 定义中包含绝对路径,则此方法会失败。

此方法禁用了默认的 dlopen(3) 逻辑。

以相对于自定义路径的方式加载库

如果您的库位于特定目录中,您可以使用以下方法加载它

<?php

// ... setup autoloads etc ...

$loader = new \rask\Libload\Loader();

$ffi = $loader->relativeTo('/my/awesome/libs')->load('./path/to/header.h');

assert($ffi instanceof \FFI);

您的头文件可能包含

#define FFI_LIB "./subdir/lib.so"

所以 $loader 会查找库在 /my/awesome/libs/subdir/lib.so

如果 FFI_LIB 定义中包含绝对路径,则此方法会失败。

此方法禁用了默认的 dlopen(3) 逻辑。

在目录中搜索库

如果您有一个具有任意结构的目录树,但知道库的名称,您可以使用以下方法搜索它

<?php

// ... setup autoloads etc ...

$loader = new \rask\Libload\Loader();

$ffi = $loader->fromDirectory('/my/awesome/libs')->load('./path/to/header.h');

assert($ffi instanceof \FFI);

如果您的目录树如下所示

/
    my/
        awesome/
            libs/
                subdir1/
                    subdir2/
                        lib5.so
                    lib3.so
                    lib4.so
                lib1.so
                lib2.so

如果您的头文件定义了

#define FFI_LIB "lib4.so"

然后 $loader 将会找到 /my/awesome/libs/subdir1/lib4.so。在更大的目录中搜索将会更慢。

此方法禁用了默认的 dlopen(3) 逻辑。

多个库加载和重置加载器

如果您想在应用程序中跨各种加载需求使用相同的加载器实例,您可能需要根据您想要加载的各种库来重置它。

您可以这样重置:

<?php

// ... setup autoloads etc ...

$loader = new \rask\Libload\Loader();

// Load using default logic
$ffi1 = $loader->load('mylib1.h');

// Load using relative to header
$ffi2 = $loader->reset()->relativeToHeader()->load('mylib2.h');

// Search a directory for the last one
$ffi3 = $loader->reset()->fromDirectory('/my/directory/path')->load('mylib3.h');

如果您在加载类型上有很大的变化,您可以在每次加载后配置实例进行重置

<?php

$loader = new \rask\Libload\Loader();

$loader->enableAutoReset();

assert($loader->isAutoResetting() === true);

// Now the instance will reset after each call to `load()`

$loader->disableAutoReset();

assert($loader->isAutoResetting() === false);

// Returned to normal manual operation for resetting

如果您不使用重置,您可以继续使用相同的加载方法来加载多个库

<?php

$loader = new \rask\Libload\Loader();

$loader = $loader->relativeTo('/my/libs');

$ffi1 = $loader->load('lib1.h');
$ffi2 = $loader->load('lib2.h');
$ffi3 = $loader->load('lib3.h');

待办事项

  • 还有其他加载库的方法吗?
  • 使其与提供 FFI_SCOPE 的头文件兼容(即使其与预加载兼容)
  • 通过不断要求 PHP 核心开发者将此功能添加到 PHP 核心FFI实现中,使此包变得无用

注释

如果您不打算编写作为包或类似内容分发的 FFI 代码,则此包可能没有用。如果您可以使用纯 \FFI::cdef() 加载您的 FFI 实例,则可能过于复杂。

目前,opcache 预加载上下文中的 FFI 库加载的文档令人困惑,因此此包在做出与预加载兼容性的承诺之前等待有关如何在生产中实际预加载 FFI 库的确认。目前请使用 CLI 应用程序。

贡献

除了支持 FFI 的 PHP CLI 安装之外,您还需要有 gccmake 可用,并且能够在生成 Linux 共享对象二进制文件(*.so)的系统上编译。这意味着您现在可能无法在 Windows 或 Mac 上运行测试。一些测试需要您在系统中提供 /lib/x86_64-linux-gnu/libc.so.6

我们需要 gccmake,以便在运行 PHPUnit 时为测试目的构建共享库。

在发送代码作为拉取请求之前

  • 尝试为您的更改添加测试(composer test),或者向维护者寻求帮助
  • 在提交之前对您的代码运行代码审查和静态分析(composer lintcomposer stan

如果您在使用此包时遇到问题,您可以提出问题。文档和清理贡献非常欢迎。功能请求是可以的,只要它们不要太偏离此包的核心目的:使加载 FFI 库变得稍微简单/容易/安全等。

如果您对如何贡献有任何疑问,您可以创建一个问题并寻求指导。

许可

MIT 许可证,请参阅 LICENSE.md