設定持久化
Plugin 想跨執行記住東西(板子接線、閾值、某個開關),不用自己讀寫檔案。
主程式已經有 config.ini;你只要實作兩支 callback,Core 幫你收、幫你送:
| callback | 什麼時候被呼叫 | 你做什麼 |
|---|---|---|
on_config_load(ctx, key, value) | 掛載時,init 成功之後、probe 之前,每個鍵一次 | 把你認得的鍵存進自己的變數 |
on_config_save(ctx, buf, buf_size) | 使用者卸載這顆 Plugin 之前,以及主程式結束存檔之前 | 把要記住的設定完整寫進 buf |
兩支都在 UI Thread 上執行(見生命週期與執行緒)。
存在 config.ini 的哪裡
你的設定固定放在 config.ini 的 [Plugin.<name>] 這一段,<name> 是 IcPlugin::name。
例如 hello_ic 的設定長這樣:
[Plugin.hello_ic]
note=hello- 同名 Plugin 的所有掛載共用同一段。 使用者在兩個分頁各掛一份,它們讀寫的是同一段,而不是各自一份。
on_config_load每次掛載各收到一次;on_config_save收集時只會問其中一個代表實例。 不要假設兩個實例之間會即時同步:B 實例要等自己下一次重新掛載才會讀到 A 剛存的新值。 - 段內是一般的
key=value行。 - 只送給主掛載(
slaves[0])。你宣告了多個位址時,其他位址的掛載不會重複收到。 - headless、widget、canvas 三種掛載型態待遇相同。
格式與長度限制
你在 on_config_save 寫進 buf 的內容要遵守:
- 每一行都是
key=value;鍵不能以[、;、#開頭;鍵與值都不能含控制字元(包含 Tab)。 - 輸出裡不要夾註解行。使用者手寫
config.ini時可以加註解、縮排,那些會被容忍;你自己輸出的內容沒有這層寬容, 有任何一行不合格,整份輸出都會被拒收(視同你回傳了非ERR_OK)。全空白的行會被跳過。 - 每一行(不含換行)最多 287 位元組。
buf_size恆為 4096(含結尾 NUL),但有效內容最多 4094 位元組,而且要以 NUL 結尾。超過或找不到 NUL 也是整份被拒收。
回傳值
on_config_save 回 ERR_OK 時,buf 必須是完整的設定,不是差量。
- 空字串代表「我沒有任何要記住的設定」,
config.ini這一段的鍵會全部被移除。 - 回任何不是
ERR_OK的值(不限負值)=「這次先別存」:Core 記一則警告,維持你上一次成功交出的內容 (本次執行從沒成功交出過,就維持磁碟上原本的),你不會丟掉已經存好的設定。 適合在你的設定正處於不完整的中間狀態時使用。 - 寧可回
ERR_BUFFER_TOO_SMALL也不要輸出半截內容。
最容易踩的兩個坑
1. 整段取代,不是合併
Host 把你這次輸出當成這一段的完整內容,你沒輸出的鍵一律被刪掉。這有一個實際後果: 使用者從新版降級回你的舊版,或者別的工具在這一段加了一個你這個版本不認得的鍵, 你若只輸出自己認得的鍵,存檔時那個鍵就永久消失了。
作法:on_config_load 收到的每一個鍵都存起來(包含你不認得的),on_config_save 時連同你自己更新過的鍵一起原樣輸出。
下面的範例就是這樣做的。
2. on_config_save 不能碰硬體
它在 UI Thread 上,而且主程式結束時會落在關閉流程的寬限期內(約 5 秒,期間不等任何工作),你擋在整個關閉流程正中央。
所以:必須快、不能阻塞,ExecSync 在這裡會回 ERR_WRONG_THREAD。
要記住的東西應該早就在你的記憶體裡(上次 on_config_load 收到的,或使用者透過 widget/自訂指令改過的),
不要想「存檔前最後讀一次硬體」。
範例
/**
* @file config_persist.c
* @brief Remembering one setting across runs with on_config_load and
* on_config_save. A reading aid for the tutorial page
* "config-persistence", not a complete plugin: the two callbacks
* would be assigned to IcPlugin::on_config_load / on_config_save.
*
* @note Kept ASCII-only on purpose and C89 so it passes the syntax-only
* guard. Only the key "note" is understood; every other key that
* shows up is kept and written back, so downgrading or a hand-edited
* config.ini never loses data (the save output REPLACES the whole
* [Plugin.<name>] section).
*/
#include <stdio.h>
#include <string.h>
#include "nexus_plugin_api.h"
/** @brief The setting this sample understands; empty until loaded. */
static char s_note[64] = "";
/** @brief Keys this sample does not understand, kept verbatim ("key=value\n" lines). */
static char s_extra[1024] = "";
/**
* @brief Receives one key=value pair from the [Plugin.<name>] section.
* @param[in] ctx Main mount context (unused).
* @param[in] key Key, leading and trailing blanks already trimmed.
* @param[in] value Value, trimmed the same way.
* @note UI Thread, after init() and before probe(). Only the primary
* mount (slaves[0]) receives it. The same key is delivered once.
*/
void NX_CALL cfg_load(NxPluginCtx ctx, const char *key, const char *value)
{
size_t used;
size_t need;
NX_UNUSED_ARG(ctx);
if (strcmp(key, "note") == 0)
{
used = strlen(value);
if (used > sizeof(s_note) - 1)
{
used = sizeof(s_note) - 1;
}
memcpy(s_note, value, used);
s_note[used] = '\0';
return;
}
/* Unknown key: remember it so on_config_save can echo it back. */
used = strlen(s_extra);
need = strlen(key) + strlen(value) + 2;
if (used + need < sizeof(s_extra))
{
sprintf(s_extra + used, "%s=%s\n", key, value);
}
}
/**
* @brief Fills buf with the COMPLETE set of settings to remember.
* @param[in] ctx Main mount context (unused).
* @param[out] buf Output buffer, already an empty string.
* @param[in] buf_size Always NX_PLUGIN_CONFIG_MAX (4096, NUL included).
* @return ERR_OK when buf holds the full settings; any other value means
* "do not save this time, keep what was saved before".
* @note UI Thread, before unload and before the host writes config.ini
* at exit. Must be fast; ExecSync is not available here.
* Every line must be key=value with no control characters and no
* comment lines; the whole text must be at most 4094 bytes.
*/
NxLong NX_CALL cfg_save(NxPluginCtx ctx, char *buf, int buf_size)
{
char line[96];
size_t len_line;
size_t len_extra;
NX_UNUSED_ARG(ctx);
sprintf(line, "note=%s\n", s_note);
len_line = strlen(line);
len_extra = strlen(s_extra);
if ((int)(len_line + len_extra) > buf_size - 2)
{
return ERR_BUFFER_TOO_SMALL; /* better an error than half an output */
}
memcpy(buf, line, len_line);
memcpy(buf + len_line, s_extra, len_extra + 1); /* +1 copies the NUL */
return ERR_OK;
}(同前面的範例一樣,這是閱讀輔助:兩支函式要自己填進 IcPlugin 的 on_config_load/on_config_save。)
版本相容:只准向後相容
- 新版本新增的鍵,你的讀取端要有自己的預設值:舊的
config.ini沒有那個鍵,不能因此出錯、崩潰或讓 Plugin 進不了probe。 - 舊版本讀到不認得的鍵要忽略它:不得報錯,也不得拒收整段。
理由同上:使用者可能把 Plugin 降版,降版後的舊版仍要能正常開機。
什麼時候不會存
on_config_save 寫的值沒進 config.ini,依序檢查:
- 回傳值不是
ERR_OK。 - 輸出有不合格的行(沒有
=、含控制字元、鍵以[/;/#開頭)。 - 輸出超過 4094 位元組。
on_config_save崩潰了(見下)。- 這是批次模式:
--batch只會呼叫on_config_load,永遠不呼叫on_config_save,也不會回寫config.ini。見Batch/headless。 - 這顆 Plugin 本次執行從沒被掛載過,沒有東西可以寫回。
另外,如果使用者手改 config.ini 讓你的那一段啟動時就不合格(例如夾了控制字元),Core 只記一則警告、讓你用內建預設值跑;
即使本次你正常回了 ERR_OK,主程式結束時也不會用預設值把那個壞掉的段覆蓋掉,使用者原本寫的內容原封不動留著,
直到他自己修好為止。
崩潰時會怎樣
兩支 callback 都在例外隔離之內,崩潰只會讓這顆 Plugin 下線,不會波及主程式,但下場不同:
on_config_load崩潰:這次掛載指令仍算成功,但主掛載立刻被標記為故障(分頁標題顯示IC:<name>(faulted))、記一筆錯誤, 不會送probe,還沒送完的其餘鍵也不再送。on_config_save崩潰:這次收集作廢(等於什麼都沒存到),該掛載被標記為故障。如果崩潰發生在使用者卸載這顆 Plugin 的收集點, 卸載本身照常完成。
x86 建置沒有這層例外隔離,見架構要對:x86/x64。