You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在CFFI中构建字节缓冲区?解决Curl回调UTF-8错误问题

在Common Lisp + CFFI中实现动态扩容缓冲区处理Curl回调

核心思路

不要在回调阶段直接转换字符串,而是将每次收到的字节片段暂存到可动态增长的C缓冲区中,等所有数据接收完成后,再一次性把完整字节序列转换成Lisp字符串,从根源避免部分UTF-8片段导致的解码错误。

实现步骤

1. 定义缓冲区状态结构体

用Lisp结构体跟踪缓冲区的核心状态:

(cffi:defstruct buffer
  (ptr (cffi:null-pointer) :type :pointer)
  (capacity 0 :type fixnum)
  (size 0 :type fixnum))

各字段作用:

  • ptr:指向C堆上分配的缓冲区内存
  • capacity:缓冲区当前的总容量(字节数)
  • size:已写入缓冲区的有效数据长度(字节数)

2. 实现缓冲区扩容逻辑

封装C的realloc函数,实现自动扩容(示例采用翻倍扩容策略,可按需调整):

(defun buffer-grow (buffer needed-size)
  (let* ((current-cap (buffer-capacity buffer))
         (new-cap (max (+ current-cap needed-size) (* current-cap 2))))
    (setf (buffer-ptr buffer) (cffi:foreign-funcall "realloc" :pointer (buffer-ptr buffer) :size new-cap :pointer)
          (buffer-capacity buffer) new-cap)))

3. 修改Curl回调函数,写入缓冲区

将回调的stream参数绑定到我们的缓冲区结构体,每次收到数据时先检查容量,不足则扩容,再复制数据:

(cffi:defcallback cb-write :unsigned-int ((ptr :pointer) (size :unsigned-int)
                                          (nmemb :unsigned-int) (stream :pointer))
  (let* ((data-size (* size nmemb))
         (buffer (cffi:mem-ref stream 'buffer)))
    ;; 检查容量,不足则扩容
    (when (> (+ (buffer-size buffer) data-size) (buffer-capacity buffer))
      (buffer-grow buffer data-size))
    ;; 将新数据复制到缓冲区末尾
    (cffi:foreign-funcall "memcpy"
                          :pointer (cffi:inc-pointer (buffer-ptr buffer) (buffer-size buffer))
                          :pointer ptr
                          :size data-size
                          :pointer)
    ;; 更新已用数据长度
    (incf (buffer-size buffer) data-size)
    ;; 返回处理的字节数,告知Curl数据已接收
    data-size))

4. 完整请求流程示例

初始化缓冲区、配置Curl、执行请求、转换数据并清理资源:

(defun fetch-large-data (url)
  (let ((buffer (make-buffer)))
    (unwind-protect
         (cffi:with-foreign-object (curl :pointer)
           ;; 初始化Curl
           (setf (cffi:mem-ref curl :pointer) (cffi:foreign-funcall "curl_easy_init" :pointer))
           (unless (cffi:null-pointer-p (cffi:mem-ref curl :pointer))
             ;; 设置请求URL
             (cffi:foreign-funcall "curl_easy_setopt" :pointer (cffi:mem-ref curl :pointer)
                                   :int 10002 ; CURLOPT_URL
                                   :string url :int)
             ;; 绑定回调函数和缓冲区
             (cffi:foreign-funcall "curl_easy_setopt" :pointer (cffi:mem-ref curl :pointer)
                                   :int 10001 ; CURLOPT_WRITEFUNCTION
                                   :pointer (cffi:callback cb-write) :int)
             (cffi:foreign-funcall "curl_easy_setopt" :pointer (cffi:mem-ref curl :pointer)
                                   :int 10004 ; CURLOPT_WRITEDATA
                                   :pointer (cffi:pointer buffer) :int)
             ;; 执行HTTP请求
             (cffi:foreign-funcall "curl_easy_perform" :pointer (cffi:mem-ref curl :pointer) :int)
             ;; 请求完成后转换完整数据为Lisp字符串
             (when (> (buffer-size buffer) 0)
               (cffi:foreign-string-to-lisp (buffer-ptr buffer) :count (buffer-size buffer) :encoding :utf-8))))
      ;; 确保释放C堆内存,避免泄漏
      (unless (cffi:null-pointer-p (buffer-ptr buffer))
        (cffi:foreign-funcall "free" :pointer (buffer-ptr buffer))))))

关键注意事项

  • 必须用unwind-protect包裹资源操作,确保无论请求成功与否,缓冲区内存都会被释放
  • 调用foreign-string-to-lisp时指定:count参数,避免转换缓冲区中未使用的空白内存
  • 扩容策略可根据实际场景调整(比如固定增量扩容)
  • 建议用cffi:defcenum定义Curl选项常量(如CURLOPT_URL),替代硬编码数值提升代码可维护性

内容的提问来源于stack exchange,提问作者Philipp Ludwig

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 22:32:30