绝美!“伽罗太华卧床”的私密视角,解锁梦幻卧室的N种可能

核心内容摘要

苏州晶体公司iOS下载安装:开启移动视界新纪元
沈娜娜苏清歌,一锅团圆,万般温情

777777:解锁幸运密码,点亮人生无限可能

引入在现代 AI 工程中Hugging Face 的 tokenizers 库已成为分词器的事实标准。

不过 Hugging Face 的 tokenizers 是用 Rust 来实现的官方只提供了 python 和 node 的绑定实现。

要实现与 Hugging Face tokenizers 相同的行为最好的办法就是自己封装 Hugging Face tokenizers 的 C 绑定从而可以被 C / C# / Java 这些高级编程语言调用。

封装 C 接口首先要说明的是要做的不是完整的封装 Hugging Face tokenizers 的 C 的 FFIForeign Function Interface接口而是封装自己需要的接口就可以了。

比如执行分词接口和计算Token的接口/* by yours.tools - online tools website : yours.tools/zh/imagetojpeg.html */ use std::ffi::CStr; use std::os::raw::c_char; use tokenizers::{PaddingParams, Tokenizer, TruncationParams}; //

定义 C 兼容的返回结构体 #[repr(C)] pub struct TokenizerResult { pub input_ids: *mut i64, pub attention_mask: *mut i64, pub token_type_ids: *mut i64, pub length: u64, } //

内部状态包装 Tokenizer struct TokenizerHandle { tokenizer: Tokenizer, // 用于 encode带 padding raw_tokenizer: Tokenizer, // 用于 count无 padding } //

辅助函数将 Rust Vec 转为 C 可拥有的指针 fn vec_to_c_ptr(vec: Veci

- *mut i64 { let mut boxed vec.into_boxed_slice(); let ptr boxed.as_mut_ptr(); std::mem::forget(boxed); // 防止 Rust 自动释放 ptr } //

创建 tokenizer #[unsafe(no_mangle)] // 禁用 name mangling让 C 能找到符号 pub extern C fn tokenizer_create(tokenizer_json_path: *const c_char) - *mut std::ffi::c_void { if tokenizer_json_path.is_null() { return std::ptr::null_mut(); } let path_cstr unsafe { CStr::from_ptr(tokenizer_json_path) }; let path_str match path_cstr.to_str() { Ok(s) s, Err(_) return std::ptr::null_mut(), }; let mut tokenizer match Tokenizer::from_file(path_str) { Ok(t) t, Err(_) return std::ptr::null_mut(), }; // 设置 padding/truncation 到 512BGE 默认 tokenizer.with_padding(Some(PaddingParams { strategy: tokenizers::PaddingStrategy::Fixed(

, ..Default::default() })); if tokenizer .with_truncation(Some(TruncationParams { max_length: 512, ..Default::default() })) .is_err() { return std::ptr::null_mut(); } let mut raw_tokenizer tokenizer.clone(); raw_tokenizer.with_padding(None); raw_tokenizer.with_truncation(None).ok(); let handle TokenizerHandle { tokenizer, raw_tokenizer, }; Box::into_raw(Box::new(handle)) as *mut std::ffi::c_void } //计算句子token #[unsafe(no_mangle)] // 禁用 name mangling让 C 能找到符号 pub extern C fn tokenizer_count(handle: *mut std::ffi::c_void, text: *const c_char) - u64 { if handle.is_null() || text.is_null() { return 0; } let handle_ref unsafe { *(handle as *mut TokenizerHandle) }; let text_cstr unsafe { CStr::from_ptr(text) }; let text_str match text_cstr.to_str() { Ok(s) s, Err(_) return 0, }; match handle_ref.raw_tokenizer.encode(text_str, true) { Ok(encoding) encoding.len() as u64, Err(_) 0, } } //

销毁 tokenizer #[unsafe(no_mangle)] pub extern C fn tokenizer_destroy(handle: *mut std::ffi::c_void) { if !handle.is_null() { unsafe { let _ Box::from_raw(handle as *mut TokenizerHandle); // Drop 自动调用 } } } //

执行分词 #[unsafe(no_mangle)] pub extern C fn tokenizer_encode( handle: *mut std::ffi::c_void, text: *const c_char, ) - TokenizerResult { let default_result TokenizerResult { input_ids: std::ptr::null_mut(), attention_mask: std::ptr::null_mut(), token_type_ids: std::ptr::null_mut(), length: 0, }; if handle.is_null() || text.is_null() { return default_result; } let handle_ref unsafe { *(handle as *mut TokenizerHandle) }; let text_cstr unsafe { CStr::from_ptr(text) }; let text_str match text_cstr.to_str() { Ok(s) s, Err(_) return default_result, }; let encoding match handle_ref.tokenizer.encode(text_str, true) { Ok(e) e, Err(_) return default_result, }; let input_ids: Veci64 encoding.get_ids().iter().map(|x| x as i

.collect(); let attention_mask: Veci64 encoding .get_attention_mask() .iter() .map(|x| x as i

.collect(); let token_type_ids: Veci64 encoding.get_type_ids().iter().map(|x| x as i

.collect(); // BGE 不需要但 C 代码传了 // let token_type_ids: Vecu32 vec![0u32; input_ids.len()]; let len input_ids.len(); // 应该是 512但更通用 TokenizerResult { input_ids: vec_to_c_ptr(input_ids), attention_mask: vec_to_c_ptr(attention_mask), token_type_ids: vec_to_c_ptr(token_type_ids), length: len as u64, } } //

释放结果内存 #[unsafe(no_mangle)] pub extern C fn tokenizer_result_free(result: TokenizerResult) { if !result.input_ids.is_null() { unsafe { let _ Vec::from_raw_parts( result.input_ids, result.length as usize, result.length as usize, ); } } if !result.attention_mask.is_null() { unsafe { let _ Vec::from_raw_parts( result.attention_mask, result.length as usize, result.length as usize, ); } } if !result.token_type_ids.is_null() { unsafe { let _ Vec::from_raw_parts( result.token_type_ids, result.length as usize, result.length as usize, ); } } }对应的 C 接口如下/* by yours.tools - online tools website : yours.tools/zh/imagetojpeg.html */ // tokenizer_result.h #pragma once struct TokenizerResult { int64_t* input_ids; int64_t* attention_mask; int64_t* token_type_ids; uint64_t length; }; #ifdef __cplusplus static_assert(std::is_standard_layout_vTokenizerResult std::is_trivially_copyable_vTokenizerResult, TokenizerResult must be C ABI compatible); #endif// hf_tokenizer_ffi.h #pragma once #include stdint.h #include tokenizer_result.h #ifdef __cplusplus extern C { #endif void* tokenizer_create(const char* tokenizer_json_path); void tokenizer_destroy(void* handle); TokenizerResult tokenizer_encode(void* handle, const char* text); uint64_t tokenizer_count(void* handle, const char* text); void tokenizer_result_free(TokenizerResult result); #ifdef __cplusplus } #endif具体的封装细节笔者就不多说了因为与本文的主题无关。

不过可以稍稍了解一下其中的原理也就是说操作系统大多数是由 C 实现的或者提供了 C 的接口。

因此绝大多数比 C 高级的编程语言都提供了与 C 交互的能力当然前提是必须得按照 C 得规范组织数据和封装接口。

比如这里的struct TokenizerResult就是一个兼容 C 的结构体#[unsafe(no_mangle)]则表明这是一个 C 语言形式的函数接口。

经典 C 封装如上接口是一个标准的 C 风格式的接口将分词器封装成一个 Handle 也就是俗称的句柄。

而后续具体的分词操作就通过这个句柄来进行包括最后对资源的释放。

在 C 中当然也可以直接使用这种形式的接口不过这样就需要遵循 C 的资源控制规则资源申请和释放必须成对出现——比如这里的tokenizer_create和tokenizer_destroy。

1 RAII 机制不过这样就会有一个问题过程式的流程中很难保证tokenizer_create和tokenizer_destroy能够成对调用例如tokenizer_create() if(...){ return; } tokenizer_destroy()只要在tokenizer_create和tokenizer_destroy之间出现分支程序提前返回就会导致资源没有释放而内存泄漏。

为了避免这个问题就需要在每次return之前都调用tokenizer_destroy()——这当然是非常不优雅的既容易忘掉又是冗余代码。

为了解决这种资源管理难题C 提供了一种强大而优雅的机制RAIIResource Acquisition Is Initialization资源获取即初始化。

它的核心思想是将资源的生命周期绑定到对象的生命周期上。

具体来说就是利用面向对象的思想将资源控制的行为封装成一个类对象并且保证资源在对象构造函数中获取在析构函数中自动释放。

由于 C 中栈对象在离开作用域时会自动调用析构函数在离开作用域时会自动调用析构函数。

因此这些资源总是可以被正确释放从根本上杜绝内存泄漏或资源泄露。

例如Tokenizer tokenizer; //...操作 if(...){ return; } //...更多操作

2 拷贝语义复习一下 C 面向对象设计的经典五法则Rule of Five如果一个类自定义了以下任意一个函数析构函数Destructor拷贝构造函数Copy Constructor拷贝赋值运算符Copy Assignment Operator移动构造函数Move Constructor移动赋值运算符Move Assignment Operator那么大概率也需要自定义另外四个函数或者显式 default / delete 来控制行为。

很多 C 程序员并不理解移动语义但这并没有关系我们可以先假定不定义移动构造函数和移动赋值运算符或者显式 default此时移动操作就会退化为拷贝语义的行为。

而关于拷贝语义绝大多数 C 程序员应该都知道这个问题当在类对象中管理资源时编译器生成的默认拷贝行为是“浅拷贝”可能导致双重释放、内存泄漏等问题因此需要自定义拷贝构造函数和拷贝赋值运算符来实现“深拷贝”的行为。

因此这个链条就很明确了因为类中需要定义析构函数所以需要同时定义拷贝构造函数和拷贝赋值运算符。

3 移动语义进一步讨论反正移动语义可以默认那么是不是只用定义拷贝语义就行了呢这个要看资源的定义如果只是管理内存资源那么这样做是没有问题的至少是安全的。

但是资源管理不仅仅指的是内存资源还可以是一些文件句柄、网络连接等等。

这些资源往往是独占性的进行深拷贝往往会出现问题。

因此就出现了 C 11 开始规定的移动语义可以安全得实现“浅拷贝”的行为。

同时还可以解决“深拷贝”的性能问题。

基于以上的思想笔者封装的分词器对象如下// HfTokenizer.h #pragma once #include string #include hf_tokenizer_ffi.h namespace hf { class Tokenizer { public: explicit Tokenizer(const std::string path); // 析构函数 ~Tokenizer() noexcept; // 禁止拷贝 Tokenizer(const Tokenizer) delete; Tokenizer operator(const Tokenizer) delete; // 移动语义 Tokenizer(Tokenizer rhs) noexcept; Tokenizer operator(Tokenizer rhs) noexcept; // 其他接口方法 // TokenizerResult Encode(const char* text) const; // uint64_t Count(const char* text) const; private: void* handle; // 来自 tokenizer_create 的指针 }; } // namespace hf// HfTokenizer.cpp #include HfTokenizer.h #include iostream namespace hf { Tokenizer::Tokenizer(const std::string path) : handle(tokenizer_create(path.c_str())) { if (!handle) { throw std::runtime_error(Failed to create tokenizer from path); } } Tokenizer::~Tokenizer() noexcept { if (handle) { tokenizer_destroy(handle); } } // 移动语义 Tokenizer::Tokenizer(Tokenizer rhs) noexcept : handle(rhs.handle) { rhs.handle nullptr; } Tokenizer Tokenizer::operator(Tokenizer rhs) noexcept { if (this ! rhs) { if (handle) { tokenizer_destroy(handle); } handle rhs.handle; rhs.handle nullptr; } return *this; } } // namespace hf如前所述因为封装的是一个句柄为了避免资源控制的麻烦就禁止掉拷贝语义// 禁止拷贝 Tokenizer(const Tokenizer) delete; Tokenizer operator(const Tokenizer) delete;进行()拷贝构造或者赋值构造看起来似乎很简单其实在代码层层嵌套之后就可能很难分析出是不是调用了默认的拷贝的行为比如函数传参、容器操作等等。

当然深拷贝的实现也不是性能最优因此干脆就直接删除掉拷贝构造函数和拷贝赋值运算符。

没有拷贝语义那么就需要移动语义来进行传递对象了。

其实移动语义没那么难我们只要把握住一点移动语义的目的是安全地实现“浅拷贝”。

以移动赋值运算符的实现来说如果要实现如下移动赋值Tokenizer A(); Tokenizer B(); B std::move(A);就需要以下的行为释放掉B管理的资源。

将A中的成员“浅拷贝”到B中让B接管A的资源。

将A中成员初始化。

具体实现就是如下所示Tokenizer Tokenizer::operator(Tokenizer rhs) noexcept { if (this ! rhs) { if (handle) { tokenizer_destroy(handle); } handle rhs.handle; rhs.handle nullptr; } return *this; }移动构造函数就更加简单了因为B对象在移动构造之前成员并没有初始化Tokenizer A(); Tokenizer B(std::move(A));因此可以省略掉释放自身资源的步骤具体实现也就是如下所示Tokenizer::Tokenizer(Tokenizer rhs) noexcept : handle(rhs.handle) { rhs.handle nullptr; }最后还有一个问题A通过移动语义转移到B了A还能使用吗不能也没必要使用A了无论是A对象和B对象其实是一个栈对象当然内部管理的数据成员可能放在堆上或者说是一个值对象这跟引用对象或者地址对象完全不同。

移动语义的本质是对象所有权的转移转移之后原对象中资源所有权就不存在了即使强行访问要么访问不到要么会程序崩溃。

高级 C 封装

1 零法则使用 RAII 机制 经典五法则来设计一个类对象还有一个优点就是使用这个类对象作为数据成员的类就不用再显式实现析构函数。

不用显式实现析构函数也就意味着不用实现拷贝语义和移动语义完全可以依赖类对象拷贝和移动的默认行为。

举例来说一个MyResource对象管理着一段内存 buffer 它的类定义为class MyResource { public: // 构造申请资源 MyResource() { data new int[100]; } // 析构释放资源 ~MyResource() { delete[] data; } // 拷贝构造深拷贝 MyResource(const MyResource other) { data new int[100]; copy(other.data, other.data 100, data); } // 拷贝赋值 MyResource operator(const MyResource other) { if (this ! other) { delete[] data; data new int[100]; copy(other.data, other.data 100, data); } return *this; } // 移动构造接管资源 MyResource(MyResource other) noexcept { data other.data; other.data nullptr; } // 移动赋值 MyResource operator(MyResource other) noexcept { if (this ! other) { delete[] data; data other.data; other.data nullptr; } return *this; } private: int* data nullptr; };但是如果我使用 std 容器vector相应的代码就可以简写为#include vector class MyResource { public: // 构造自动分配内存 MyResource() : data(

{} // vectorint 自动初始化为 100 个元素 // ✅ 无需显式定义析构函数 // ✅ 无需自定义拷贝构造 / 拷贝赋值 // ✅ 无需自定义移动构造 / 移动赋值 // 编译器自动生成的版本已正确、高效、异常安全 private: std::vectorint data; // RAII 自动管理内存 };这不是因为vector使用了什么魔法而是vector本身就是使用了 RAII 机制 经典五法则来设计的一个模板类对象在MyResource对象进行拷贝或者移动的时候作为数据成员std::vectorint data也会采取同样的拷贝或者移动的行为并且默认的、由编译器自动生成的版本就可以正确处理。

以上这个思想就是现代 C 更推荐的零法则Rule of Zero尽量不要手动管理资源而是使用 RAII 类型让编译器自动生成所有特殊成员函数。

而这个 RAII 类型可以是 std 的任何容器对象、智能指针也可以是自己按照五法则实现的类对象。

2 智能指针回到本文引入的问题如果我的分词器实现不像写拷贝语义和移动语义怎么办呢毕竟都是样板代码写不好还容易出问题。

此时我们就可以使用智能指针unique_ptr。

常规意义上我们都知道智能指针可以在没有任何其他对象引用的情况下自动delete其实智能指针还可以自定义资源的释放行为#pragma once #include memory #include string namespace hf { class Tokenizer { public: explicit Tokenizer(const std::string path); // 编译器自动生成 // - 析构函数 // - 移动构造 / 移动赋值 // - 禁止拷贝因为 unique_ptr 不可拷贝 private: std::unique_ptrvoid, void (*)(void*) handle; }; } // namespace hf#include HfTokenizer.h #include stdexcept #include hf_tokenizer_ffi.h namespace hf { static void HandleDeleter(void* handle) noexcept { if (handle) { tokenizer_destroy(handle); } } Tokenizer::Tokenizer(const std::string path) : handle(tokenizer_create(path.c_str()), HandleDeleter) { if (!handle) { throw std::runtime_error(Failed to create tokenizer from path); } } } // namespace hf如上实现所示函数HandleDeleter就是std::unique_ptrvoid, void (*)(void*) handle的自定义析构行为在类对象析构的时候就会自动调用这个函数释放资源。

既然资源被智能托管了那么自然就不用写析构函数析构函数不用写那么拷贝构造函数、拷贝赋值运算符、移动构造函数以及移动赋值运算符都可以不用实现全部可以依赖编译器自动生成。

当然由于unique_ptr只能移动不能拷贝Tokenizer也就只能移动不能拷贝。

5.

总结最后笔者就给出 C 封装 C FFI 接口的完整实现如下所示// HfTokenizer.h #pragma once #include memory #include string #include tokenizer_result.h namespace hf { class Tokenizer { public: explicit Tokenizer(const std::string path); // 编译器自动生成 // - 析构函数调用 Deleter // - 移动构造 / 移动赋值 // - 禁止拷贝因为 unique_ptr 不可拷贝 // 其他接口方法 uint64_t Count(const std::string text) const; // 向量化 using ResultPtr std::unique_ptrTokenizerResult, void (*)(TokenizerResult*); ResultPtr Encode(const std::string text) const; private: std::unique_ptrvoid, void (*)(void*) handle; }; } // namespace hf// HfTokenizer.cpp #include HfTokenizer.h #include stdexcept #include hf_tokenizer_ffi.h namespace hf { static void HandleDeleter(void* handle) noexcept { if (handle) { tokenizer_destroy(handle); } } static void ResultDeleter(TokenizerResult* p) noexcept { if (p) { tokenizer_result_free(*p); delete p; } } Tokenizer::Tokenizer(const std::string path) : handle(tokenizer_create(path.c_str()), HandleDeleter) { if (!handle) { throw std::runtime_error(Failed to create tokenizer from path); } } uint64_t Tokenizer::Count(const std::string text) const { return tokenizer_count(handle.get(), text.c_str()); } Tokenizer::ResultPtr Tokenizer::Encode(const std::string text) const { auto result std::make_uniqueTokenizerResult( tokenizer_encode(handle.get(), text.c_str())); return {result.release(), ResultDeleter}; }; } // namespace hf不仅是句柄连传递的数据对象笔者都托管给智能指针从而避免大量写特殊成员函数这些样板代码。

不得不说RAII 的设计思路非常精妙同时保证了安全性与简洁性给人一种回归编程原始状态的感觉。

所谓“大道至简”不是代码越繁复就越安全也不是代码越抽象就越厉害真正好的代码是在正确性、可维护性与简洁性之间取得平衡让资源管理如呼吸般自然而非负担。

桃花源(原:小黄人)1.1.0-桃花源(原:小黄人应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123