Skip to content

Fix native candidate and schema list handling#1

Closed
amorphobia wants to merge 1 commit into
rimeinn:masterfrom
amorphobia:master
Closed

Fix native candidate and schema list handling#1
amorphobia wants to merge 1 commit into
rimeinn:masterfrom
amorphobia:master

Conversation

@amorphobia

Copy link
Copy Markdown
Member

背景

在跨 DLL 调用 librime 候选迭代 API 时,RimeCandidateListIterator 直接使用包含托管字符串字段的 RimeCandidate 接收原生结构。

候选项中的字符串指针由 librime 管理,并可能在迭代过程中被替换或在迭代结束后失效。直接依赖自动封送会导致生命周期边界不明确,也可能在原生指针失效后读取数据。

此外,Switcher 的 Schema 列表处理存在释放路径不完整和 selected list 调用错误的问题。

修改内容

  • 新增与原生布局一致的 NativeRimeCandidate
  • 在候选迭代期间立即将原生字符串指针复制为托管字符串。
  • 使用 try/finally 保证始终调用 CandidateListEnd
  • 使用 try/finally 保证 Switcher Schema 列表始终通过 SchemaListDestroy 释放。
  • 修复 GetSelectedSchemaList 错误调用 GetAvailableSchemaList 的问题。

影响

  • 消除候选数据跨 DLL 边界时对原生指针生命周期的依赖。
  • 避免候选迭代或 Schema 转换异常时泄漏原生资源。
  • GetSelectedSchemaList 现在返回正确的已选 Schema 列表。
  • RimeCandidateListIterator.Candidate 从公共字段调整为只读属性。现有源码读取方式不变,但已经针对旧程序集编译的消费者需要重新编译。

- copy candidate data before iterator-owned pointers become invalid
- guarantee candidate iterators and schema lists are released
- call the correct API for selected switcher schemas
@amorphobia

Copy link
Copy Markdown
Member Author

让 AI 写了个例子,看看上次 new 出来的 text 地址是不是下次 delete 的地址

probe.cpp
#include <cstdint>
#include <cstdio>
#include <cstring>

struct Candidate {
    char* text;
    char* comment;
    void* reserved;
};

struct Iterator {
    std::uintptr_t ptr;
    int index;
    Candidate candidate;
};

static const char* const kTexts[] = { "first", "second", "third" };
static const char* const kComments[] = { "one", "two", "three" };
static constexpr int kCount = sizeof(kTexts) / sizeof(kTexts[0]);

static char* g_last_owned_text = nullptr;

static char* copy_string(const char* source) {
    const size_t length = std::strlen(source) + 1;
    auto* copy = new char[length];
    std::memcpy(copy, source, length);
    return copy;
}

extern "C" __declspec(dllexport)
int candidate_begin(Iterator* iterator) {
    std::memset(iterator, 0, sizeof(*iterator));
    iterator->index = -1;
    return 1;
}

extern "C" __declspec(dllexport)
int candidate_next(Iterator* iterator) {
    ++iterator->index;

    if (iterator->index >= kCount) {
        return 0;
    }

    // Mirrors librime: the iterator owns and replaces these pointers.
    std::fprintf(
        stderr,
        "[next] deleting text=%p, comment=%p\n",
        static_cast<void*>(iterator->candidate.text),
        static_cast<void*>(iterator->candidate.comment));
    std::fflush(stderr);

    delete[] iterator->candidate.text;
    delete[] iterator->candidate.comment;

    iterator->candidate.text = copy_string(kTexts[iterator->index]);
    iterator->candidate.comment = copy_string(kComments[iterator->index]);
    iterator->candidate.reserved = nullptr;
    g_last_owned_text = iterator->candidate.text;

    std::fprintf(
        stderr,
        "[next] allocated text=%p\n",
        static_cast<void*>(iterator->candidate.text));
    std::fflush(stderr);

    return 1;
}

extern "C" __declspec(dllexport)
void candidate_end(Iterator* iterator) {
    std::fprintf(
        stderr,
        "[end] deleting text=%p, comment=%p\n",
        static_cast<void*>(iterator->candidate.text),
        static_cast<void*>(iterator->candidate.comment));
    std::fflush(stderr);

    delete[] iterator->candidate.text;
    delete[] iterator->candidate.comment;
    std::memset(iterator, 0, sizeof(*iterator));
    g_last_owned_text = nullptr;
}

extern "C" __declspec(dllexport)
void* candidate_last_owned_text() {
    return g_last_owned_text;
}
probe.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <None Update="probe.dll" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>

</Project>
Program.cs
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct Candidate
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string? Text;

    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string? Comment;

    public IntPtr Reserved;
}

[StructLayout(LayoutKind.Sequential)]
public struct Iterator
{
    public UIntPtr Ptr;
    public int Index;

    [MarshalAs(UnmanagedType.Struct)]
    public Candidate Candidate;
}

internal static class Native
{
    [DllImport("probe", CallingConvention = CallingConvention.Cdecl)]
    internal static extern int candidate_begin(ref Iterator iterator);

    [DllImport("probe", CallingConvention = CallingConvention.Cdecl)]
    internal static extern int candidate_next(ref Iterator iterator);

    [DllImport("probe", CallingConvention = CallingConvention.Cdecl)]
    internal static extern void candidate_end(ref Iterator iterator);

    [DllImport("probe", CallingConvention = CallingConvention.Cdecl)]
    internal static extern IntPtr candidate_last_owned_text();
}

internal static class Program
{
    private static void Main()
    {
        var iterator = default(Iterator);

        if (Native.candidate_begin(ref iterator) == 0)
        {
            throw new InvalidOperationException("begin failed");
        }

        try
        {
            Console.WriteLine("Calling first next...");
            Native.candidate_next(ref iterator);

            Console.WriteLine($"Managed text: {iterator.Candidate.Text}");
            Console.WriteLine(
                $"Native-owned pointer: 0x{Native.candidate_last_owned_text().ToInt64():X}");

            Console.WriteLine();
            Console.WriteLine("Calling second next...");
            Native.candidate_next(ref iterator);

            Console.WriteLine($"Managed text: {iterator.Candidate.Text}");
            Console.WriteLine(
                $"Native-owned pointer: 0x{Native.candidate_last_owned_text().ToInt64():X}");
        }
        finally
        {
            Native.candidate_end(ref iterator);
        }
    }
}

编译 DLL

cl /LD /EHsc probe.cpp /Fe:probe.dll

运行

dotnet run

我的结果

Calling first next...
[next] deleting text=0000000000000000, comment=0000000000000000
[next] allocated text=000001F9000C9B90
Managed text: first
Native-owned pointer: 0x1F9000C9B90

Calling second next...
[next] deleting text=000001F9000C9DB0, comment=000001F9000C9D70
[next] allocated text=000001F9000C9D00
Managed text: second
Native-owned pointer: 0x1F9000C9D00
[end] deleting text=000001F9000C9B70, comment=000001F9000C9C00

C++ 一开始 new 出来的地址是 0x1F9000C9B90, 调用 next 的时候 delete 的地址是 0x1F9000C9DB0;然后第二次 new 出来的地址是 0x1F9000C9D00,最终 delete 的地址是 0x1F9000C9B70

@amorphobia amorphobia closed this Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant