编程开发与计算机科学

一直被错误设计的 HTTP API 参数校验模式

传统 Web framework 或者 HTTP API framework 对于接口参数校验的模式通常如下:

  1. 为每个参数绑定一个 Validator 定义
  2. 调用 Validation 对所有参数进行校验
  3. 如果验证失败则返回客户端一个错误列表,包含每一个验证失败的参数以及失败原因

但上述模式是一种错误的设计,是一种职责错位。接口调用本身隐含着契约,应该由调用者负责遵守契约,被调用者负责拒绝违反契约的调用。而不是被调用者逐项校验参数、聚合结果并返回一份错误列表。对于违反调用契约的参数,接口应当尽早失败(fail-fast),直接拒绝调用并返回 ContractViolation 错误。换句话说,接口不负责用户表单验证。如有类似需求,应该另外设计一个专用的用户表单验证接口。

以网页应用为例,在调用后端接口前,前端代码应该保证调用参数符合契约定义,而不应该依赖后端返回的错误消息列表来纠正请求参数。这种错误消息列表本质上是面向人类用户阅读的,仅在验证用户输入时才有意义。因此,应该将验证用户输入的功能从业务接口中剥离。用户输入的验证逻辑可以完全集成在前端代码里,也可以单独设计一个针对用户输入内容的验证接口。

当然,这一切的前提是接口契约必须有明确、规范且可供调用方遵循的文档定义。

……

    Nim 编译器的古怪问题

    Nim 是一门高度灵活的编程语言,其代码具有很强的多态与组合能力,尤其在使用 macro 时,语义往往需要在编译期展开后才能最终确定。这种高自由度的设计在增强抽象能力的同时,也引入了显著的语义不确定性,使得程序行为更依赖上下文推导与编译期分析。

    最近碰到一个令人迷惑的 Nim 编译器问题。在某个模块中,我定义了如下的宏,用于提供对 config 对象的读写封装:

    ……

    VSCode 远程连接 Vagrant 虚拟机开发环境

    VSCode 远程连接 Vagrant 虚拟机和连接 WSL 开发环境大同小异。只是要注意一个小问题:本地可能存在多个 Vagrant 开发环境共享同一个 IP 端口,即 127.0.0.1:2222,这会导致 SSH 连接时报安全警告。在 %USERPROFILE%\.ssh\config 文件里添加如下配置:

    Host Vagrant
      HostName 127.0.0.1
      Port 2222
      User vagrant
      StrictHostKeyChecking no
      UserKnownHostsFile NUL
    %USERPROFILE%\.ssh\config

    ……

    RegGetValueW 返回字符串数据大小不一致

    Windows API RegGetValueW 函数的原型如下:

    c++LSTATUS RegGetValueW(
      [in]                HKEY    hkey,
      [in, optional]      LPCWSTR lpSubKey,
      [in, optional]      LPCWSTR lpValue,
      [in, optional]      DWORD   dwFlags,
      [out, optional]     LPDWORD pdwType,
      [out, optional]     PVOID   pvData,
      [in, out, optional] LPDWORD pcbData
    );

    使用该 API 获取字符串值时,是否传递 pvData 参数会影响 pcbData 输出的值大小。

    ……

    通过 GitHub Actions 编译 Lazarus 项目

    理论上 Lazarus 是支持交叉编译的,但官方发行的默认版本并没有打包不同平台所依赖的各组件。比如在 Windows 平台上将编译目标设为 Linux 时,编译器会报错:Can’t find unit system used by fcllaz ——缺少 Linux 平台上的预编译文件。

    虽然可以使用 fpcupdeluxe (GitHub) 项目安装支持交叉编译的 Lazarus 工具链,不过这个工具似乎不支持命令行环境下的配置和安装。

    ……

    Lazarus 项目支持多语言国际化(进阶篇)

    在之前的一篇文章中简单介绍了如何在 Lazarus 项目中启用多语言国际化。不过使用 DefaultTranslator 或 SetDefaultLang() 无法实现运行时动态切换 GUI 的语言。更准确地说,在程序中首次调用 LCLTranslator.SetDefaultLang(),并将参数 ForceUpdate 设为 True,是能够在运行时修改 GUI 语言的,但若再次调用便无效了。

    查看 SetDefaultLang() 源代码:

    pascalfunction SetDefaultLang(Lang: string; Dir: string = ''; LocaleFileName: string = ''; ForceUpdate: boolean = true): string;
    { Arguments:
      Lang - language (e.g. 'ru', 'de', 'zh_CN'); empty argument is default language.
      Dir - custom translation files subdirectory (e.g. 'mylng'); empty argument means searching only in predefined subdirectories.
      LocaleFileName - custom translation file name; empty argument means that the name is the same as the one of executable.
      ForceUpdate - true means forcing immediate interface update. Only should be set to false when the procedure is
        called from unit Initialization section. User code normally should not specify it.
    }
    var
      lcfn: string;
      LocalTranslator: TUpdateTranslator;
      i: integer;
    
    begin
      Result := '';
      LocalTranslator := nil;
      // search first po translation resources
      try
        lcfn := FindLocaleFileName('.po', Lang, Dir, LocaleFileName, Result);
        if lcfn <> '' then
        begin
          Translations.TranslateResourceStrings(lcfn);
          LocalTranslator := TPOTranslator.Create(lcfn);
        end
        else
        begin
          // try now with MO translation resources
          lcfn := FindLocaleFileName('.mo', Lang, Dir, LocaleFileName, Result);
          if lcfn <> '' then
          begin
            GetText.TranslateResourceStrings(UTF8ToSys(lcfn));
            LocalTranslator := TDefaultTranslator.Create(lcfn);
          end;
        end;
      except
        Result := '';
        lcfn := '';
      end;
    
      if lcfn<>'' then
        TranslateLCLResourceStrings(Lang, lcfn);
    
      if LocalTranslator<>nil then
      begin
        if Assigned(LRSTranslator) then
          LRSTranslator.Free;
        LRSTranslator := LocalTranslator;
    
        // Do not update the translations when this function is called from within
        // the unit initialization.
        if ForceUpdate=true then
        begin
          for i := 0 to Screen.CustomFormCount-1 do
            LocalTranslator.UpdateTranslation(Screen.CustomForms[i]);
          for i := 0 to Screen.DataModuleCount-1 do
            LocalTranslator.UpdateTranslation(Screen.DataModules[i]);
        end;
      end;
    end;
    lcltranslator.pas

    该函数首先查找本地的 .po 或 .mo 翻译文件;然后,将程序的资源字符串翻译成本地语言;接下来,创建一个新的 TUpdateTranslator 对象替代全局对象。

    ……

    Free Pascal Hack 之访问对象的 protected 成员

    默认情况下,Free Pascal 类中 protected 成员只能被当前类、子类以及同一个 Unit 中的代码访问。然而在实践中,往往会碰到访问其他 Unit 中定义的类实例对象的 protected 成员的需求。通常我们会通过类继承以及修改原始类成员的代码来暴露 protected 成员,然而当该对象来自第三方库,甚至 FCL 和 LCL 时,情况就会比较棘手。有一种 Hack 方法可以实现这个需求,且不用修改原始 Unit 的代码,没有额外副作用。

    ……

    GetSystemMenu() 可能损坏其他进程的窗口菜单

    通过 Windows API GetSystemMenu 获取窗口系统菜单(即窗口标题栏右键菜单)句柄,可以实现自定义窗口菜单的功能。但若获取的窗口菜单句柄来自其他进程的窗口,便会引发问题。

    ……

    有趣的编程语言 Red

    Red 项目最早始于 2011 年,项目由 Nenad Rakocevic 主导。Red 语言是一种同时具有函数式、声明式、符号式特性的现代编程语言,其目标是构建成一个全栈编程语言。Red 语言受到 Rebol 语言启发,两者语法高度相似。和 Rebol 语言只能解释执行不同,Red 语言还提供了 AOT 编译(生成独立的可执行文件)功能,在未来还将支持 JIT 编译。

    Red 语言的主要特性:

    ……

    Free Pascal 链接 C/C++ 静态库

    本文提供的方法仅在 Windows 中测试通过,未在 Linux 中进行过测试,但理论上也能使用。

    Free Pascal 中可以用非常简单的代码链接到动态库:

    pascalfunction MyAdd(x, y: Integer): Integer; external 'mylib.dll' name 'MyAdd';

    另外也可以链接到静态库,这样分发程序的时候就不用带上一堆 DLL 文件了。

    首先将 C 代码编译为静态库,然后在 Lazarus > 项目选项 > 编译器选项 > 路径 > 库 (-Fl) 中输入静态库存放的路径。此处以 C 编写的静态库导出 MyAdd() 函数为例,演示如何在 Free Pascal 中调用它。

    ……