编程开发与计算机科学

一直被错误设计的 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 项目中启用多语言国际化。不过使用 DefaultTranslatorSetDefaultLang() 无法实现运行时动态切换 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 中调用它。

    ……