Mojo 学习 —— 导入外部函数

Mojo 学习 —— 导入外部函数

前言

Mojo 支持从动态链接库中导入函数,可以很方便地使用其他编程语言来对 Mojo 功能进行扩展。

这一功能主要通过 sys 标准库中的 ffi 模块中的结构体和函数实现的。

from sys.ffi import DLHandle, external_call

DLHandle

该结构体可以代表可加载和卸载的动态链接库。即在初始化时加载,在删除对象时卸载。

可以在构建对象时指定动态链接库的路径。例如,从系统中查找 SDL 的动态链接库

fn get_sdl_lib_path() -> StringLiteral:
    if (info.os_is_linux()):
        var lib_path = '/usr/lib/x86_64-linux-gnu/libSDL2.so'
        try:
            with open('/etc/os-release', 'r') as f:
                var release = f.read()
                if (release.find('Ubuntu') < 0):
                    lib_path = '/usr/lib64/libSDL2.so'
        except:
            print("Can't detect Linux version")
        return lib_path
    if (info.os_is_macos()):
        return '/opt/homebrew/lib/libSDL2.dylib'
    return ""

参考 mojo-sdl,一个将 Mojo 绑定到 SDL2 的库。

当然,我们也可以使用自定义的动态链接库。例如,我们在本地创建一个 demo.c 文件,用于计算浮点向量中的最大值

#include <stdio.h>
#include <stdlib.h>

double array_max(double *input, size_t nelem) {
    double max = 0;
    for (size_t i = 0; i < nelem; i++) {
        if (input[i] > max) {
            max = input[i];
        }
    }
    return max;
    printf("max: %f", max);
}

然后将该程序编译成动态链接库

gcc -shared -fPIC demo.c -o libdemo.so

然后就可以在 Mojo 中导入使用了

from sys.ffi import DLHandle, external_call

alias array_max_type = fn(UnsafePointer[Float64], Int64) -> Float64

fn main():
    var handle = DLHandle('./libdemo.so')
    var array_max = handle.get_function[array_max_type]('array_max')

    var data = List[Float64](-1.732, 1.414, 3.1415, 0.618, 2.732)

    print('Max:', array_max(data.data, len(data)))

我们创建 DLHandle 对象时,可以指定绝对路径或相对路径。

调用该对象的 get_function 函数来加载动态链接库中的函数,将要导入的函数名称作为字符串参数。

get_function 需要指定函数的形式,这里我们使用 alias 定义了函数形式。

运行上面的代码将会输出向量的最大值。例如

Max: 3.1415000000000002

external_call

我们还可以使用 external_call 函数来导入外部函数。

对于 C 标准库中的函数(libc)可以直接导入。例如

alias c_void = UInt8
alias c_char = UInt8
alias c_int = Int32
alias c_size_t = Int

fn to_char_ptr(s: String) -> Pointer[c_char]:
    # Hello World0
    var ptr = Pointer[c_char]().alloc(len(s) + 1)
    for i in range(len(s)):
        ptr.store(i, ord(s[i]))
    return ptr

fn c_charptr_to_string(s: Pointer[c_char]) -> String:
    return String(s.bitcast[Int8](), strlen(s) + 1)

fn strlen(s: Pointer[c_char]) -> c_size_t:
    return external_call["strlen", c_size_t, Pointer[c_char]](s)
    
fn main():
    var name = String('Hello World')
    var name_ptr = to_char_ptr(name)
    print('ptr len:', strlen(name_ptr))
    print('String:', c_charptr_to_string(name_ptr))
# ptr len: 11
# String: Hello World

注意,将指针转换为字符串时需要包含一个终止符(0

也可以使用 external_call 从第三方静态库中导入函数。这里需要设置 ld 参数并使用 mojo build

这里我们参考 mojo-ffi 里面的内容。直接使用里面的两个文件 ldmojoc,并使用 Makefile 封装流程。

我将 Makefile 修改为

all: call_demo libdemo.a libdemo.so run

libdemo.a: demo.c
	gcc -static -c demo.c -o libdemo.a

libdemo.so: demo.c
	gcc -shared -fPIC demo.c -o libdemo.so

call_demo: call_demo.mojo libdemo.a
	./scripts/mojoc call_demo.mojo -Slibdemo.a -o call_demo

run: FORCE call_demo
	./call_demo

clean:
	rm -f libdemo.a call_demo

FORCE:

其中,同级目录下 script 文件夹中放置了 mojocld 文件,c 代码在 demo.c 中,可以根据需要修改,call_demo.mojo 文件中写对应的 Mojo 代码。

例如,我们在 demo.c 中添加如下代码

#include <stdio.h>
#include <stdlib.h>

void say_hello(const char *name) {
    printf("Hello %s", name);
}

call_demo.mojo 中测试,并在当前目前下运行 make 执行程序

from sys.ffi import DLHandle, external_call

alias c_char = UInt8

# `String` to `Pointer[c_char]`
fn to_char_ptr(s: String) -> Pointer[c_char]:
    var ptr = Pointer[c_char]().alloc(len(s))
    for i in range(len(s)):
        ptr.store(i, ord(s[i]))
    return ptr

fn say_hello(name: String):
    return external_call['say_hello', NoneType](to_char_ptr(name))
    
fn main():
    say_hello('World')
# Hello World

我们再来一个复杂的例子,涉及结构体与指针。

我们用 C 编写一个链表结构,并添加几个函数,用于将向量转换为链表以及链表转换为向量。例如

typedef struct Node {
    int data;
    struct Node* next;
} Node;

Node* create_node(int data) {
    Node* new_node = (Node*)malloc(sizeof(Node));
    new_node->data = data;
    new_node->next = NULL;
    return new_node;
}

Node *from_values(int* list, size_t size) {
    Node *head = create_node(list[0]);
    Node *ptr = head;
    for (int i = 1; i < size; ++i) {
        ptr->next = create_node(list[i]);
        ptr = ptr->next;
    }
    return head;
}

int *get_values(Node *head) {
    int *vec;
    int i = 0;
    Node *ptr = head;
    while (ptr != NULL) {
        vec[i] = ptr->data;
        ptr = ptr->next;
        i++;
    }
    return vec;
}

Mojo 中使用这些函数。首先定义结构体和函数

alias c_int = Int32

@register_passable('trivial')
struct Node:
    var data: c_int
    var next: Pointer[Self]

fn from_values(data: UnsafePointer[c_int], size: c_int) -> Pointer[Node]:
    return external_call['from_values', Pointer[Node], UnsafePointer[c_int], c_int](data, size)

fn get_value(head: Pointer[Node]) -> Pointer[c_int]:
    return external_call['get_values', Pointer[c_int], Pointer[Node]](head)

from_values 可以将 Mojo 中的整数向量装换为链表,并返回链表的头部指针。

get_value 用于从头部指针中遍历获取链表的值,并保存成了整数指针向量。

使用示例

def main():
    var vec = List[c_int](1, 2, 3, 4, 5, 6)
    # 创建并获取链表头指针
    var node = from_values(vec.data, 6)
    # 遍历链表
    print('Start', end=' ')
    while node:
        print("->", node[].data, end=' ')
        node = node[].next
    print('-> End')
    # 避免数据提前释放
    _ = vec
# ./scripts/mojoc call_demo.mojo -Slibdemo.a -o call_demo
# ./call_demo
# Start -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> End

或者

def main():
    var vec = List[c_int](1, 2, 3, 4, 5, 6)
    # 创建并获取链表头指针
    var node = from_values(vec.data, 6)
    # 根据头指针遍历数据
    var res = get_value(node)
    for i in range(len(vec)):
        print(res[i], end=' ')
    # 避免数据提前释放
    _ = vec
# ./scripts/mojoc call_demo.mojo -Slibdemo.a -o call_demo
# ./call_demo
# 1 2 3 4 5 6

注意

我们在最后一行添加了 _ = vec,避免 vec 提前释放导致数据返回结果出现问题。具体可以返回去看生命周期那一章。

使用这种方式可以很容易将之前的 C/C++ 代码封装为 Mojo 代码,扩展 Mojo 的功能。

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值