标签 Kotlin/Native 下的文章

省流:support non UTF-8 mingw filesystem by magic-cucumber · Pull Request #1758 · square/okio · GitHub

前情提要

使用 kotlin-native,搭配 cliktokio 写小玩具时,在自己的 mac 上运行的好好的,但是在好盆友的电脑上就报错…

相信各位佬们看到这里一定会会心一笑。毕竟就算是我在看到这种低级错误时的第一反应也是:在程序里转个码不就行了么?

但是如果问题就这么简单的话这个 pr 就不会诞生了…

踩坑记录

一开始以为是编码问题,让好盆友使用 chcp 65001 切换到 utf-8 编码,报错。

然后引入了 fleeksoft-charset 库,并自己编写平台函数实现编码检测,然后将路径字符串转换为正确的编码后再进行处理,依然报错

jvm-windows 环境下执行程序,不报错。于是将怀疑的目光转向了 okio 库本身…

源码窥探


根据报错堆栈,问题出在 commonMetadata() 函数:

@Throws(IOException::class) internal fun FileSystem.commonMetadata(path: Path): FileMetadata {
  return metadataOrNull(path) ?: throw FileNotFoundException("no such file: $path")
}

由于报错仅发生在 windows 平台,而 kotlin-native 在 windows 上使用 minGW,故查看 PosixFileSystem

这里是一层包装方法,为 unix/mingw 提供了不同的实现,我们仅关注 mingw 部分。

 override fun metadataOrNull(path: Path) = variantMetadataOrNull(path)
internal actual fun PosixFileSystem.variantMetadataOrNull(path: Path): FileMetadata? {
  return memScoped {
    val stat = alloc<_stat64>()
    if (_stat64(path.toString(), stat.ptr) != 0) {
      if (errno == ENOENT) return null throw errnoToIOException(errno)
    }
    return@memScoped FileMetadata(
      isRegularFile = stat.st_mode.toInt() and S_IFMT == S_IFREG,
      isDirectory = stat.st_mode.toInt() and S_IFMT == S_IFDIR,
      symlinkTarget = null,
      size = stat.st_size,
      createdAtMillis = stat.st_ctime * 1000L,
      lastModifiedAtMillis = stat.st_mtime * 1000L,
      lastAccessedAtMillis = stat.st_atime * 1000L,
    )
  }
}

可以看到它的底层是调用了 stat64 函数 (尽管它不是那么的标准),而问题在于 stat 系列函数要求传入窄字符,对于 posix-api,有专门的_w 前缀函数提供了宽字符版本。

许多 Windows API 函数具有 “A”(ANSI)和 “W”(宽、Unicode)版本。 “A” 版本基于 Windows 代码页处理文本,而 “W” 版本处理 Unicode 文本。

摘自代码页 - Win32 apps | Microsoft Learn

既然如此,我们只需要将其改写成 宽字符 版本的就可以了

if (_wstat64(path.toString().wcstr, stat.ptr) != 0) {

… 吗?

我单独写了一个测试,在本机提前创建中文字符文件,并调用 metadataOrNull() 函数。完美运行,并且没有任何错误

然而当我将编译的 snapshot 包发布到本地供我的小玩具引用时,它还是报了一模一样的错,,只不过被报错的文件从我输入的文件夹变成了其中的一个子文件。

这只能说明 okio.Path 本身就有问题。


请允许我我贴一下我编写的业务函数:

fun Path.walk(consumer: (Path) -> Unit) {
    if (isFile) {
        consumer(this)
        return
    }
    for (child in list()) {
        child.walk(consumer)
    }
}

val Path.isFile
    get() = metadata().isRegularFile

fun Path.list() = FileSystem.SYSTEM.list(this)

我的程序涉及到递归遍历文件夹,而正好遍历到子文件就报错了,相信佬 u 们一定会将目光聚集到 list() 函数。

private fun list(dir: Path, throwOnFailure: Boolean): List<Path>? {
    val opendir = opendir(dir.toString())
      ?: if (throwOnFailure) throw errnoToIOException(errno) else return null try {
      val result = mutableListOf<Path>()
      val buffer = Buffer()

      set_posix_errno(0) // If readdir() returns null it's either the end or an error. while (true) {
        val dirent: CPointer<dirent> = readdir(opendir) ?: break val childPath = buffer.writeNullTerminated(
          bytes = dirent[0].d_name,
        ).toPath(normalize = true)

        if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) {
          continue // exclude '.' and '..' from the results.
        }

        result += dir / childPath
      }

      if (errno != 0) {
        if (throwOnFailure) {
          throw errnoToIOException(errno)
        } else {
          return null
        }
      }

      result.sort()
      return result
    } finally {
      closedir(opendir) // Ignore errno from closedir.
    }
  }

注意到这个 list 函数是直接写在 PosixFileSystem.kt 里的,它没有针对 unix/mingw 的变种,且仍然使用了 窄字符 api。因此要想修复它也很简单,将这个函数仿照其他的函数,根据 unix/mingw 拆分为对应的变种。

受限于篇幅,这里只贴出 windows 部分的函数

internal actual fun PosixFileSystem.variantList(dir: Path, throwOnFailure: Boolean): List<Path>? {
  val opendir = _wopendir(dir.toString().wcstr)
    ?: if (throwOnFailure) throw errnoToIOException(errno) else return null try {
    val result = mutableListOf<Path>()
    set_posix_errno(0) // If readdir() returns null it's either the end or an error. while (true) {
      val dirent: CPointer<_wdirent> = _wreaddir(opendir) ?: break val childPath = dirent[0].d_name.toKString().toPath()

      if (childPath == SELF_DIRECTORY_ENTRY || childPath == PARENT_DIRECTORY_ENTRY) {
        continue // exclude '.' and '..' from the results.
      }

      result += dir / childPath
    }

    if (errno != 0) {
      if (throwOnFailure) {
        throw errnoToIOException(errno)
      } else {
        return null
      }
    }

    result.sort()
    return result
  } finally {
    _wclosedir(opendir) // Ignore errno from closedir.
  }
}


其他的函数是否也存在这个问题呢?答案是有的,这里便不再赘述。
具体参考话题上方的 PR 链接。

彩蛋

我们都知道,在 windows 系统中,如果如果该文件被占用,则删除此文件的操作会失败。

而在我调试我的小玩具中,当删除临时文件失败后,报错信息居然也是乱码?

于是我在这里贴出负责读取 IO 错误 并 抛出异常的代码…

internal fun errnoToIOException(errno: Int): IOException {
  val message = strerror(errno)
  val messageString = if (message != null) {
    Buffer().writeNullTerminated(message).readUtf8()
  } else {
    "errno: $errno"
  }
  return when (errno) {
    ENOENT -> FileNotFoundException(messageString)
    else -> IOException(messageString)
  }
}

internal fun Buffer.writeNullTerminated(bytes: CPointer<ByteVarOf<Byte>>): Buffer = apply {
  var pos = 0 while (true) {
    val byte = bytes[pos++].toInt()
    if (byte == 0) {
      break
    } else {
      writeByte(byte)
    }
  }
}

可以看出来它也是直接调用的窄字符 api。但问题是谁能想到 message 是本地化的文本呢…

至此,本帖正式完结


📌 转载信息
原作者:
kagg886
转载时间:
2026/1/6 11:40:24