Technisch artikel

REGEXTEST en REGEXEXTRACT in Delphi via statische PCRE2

HotXLS evalueert de reguliere-expressiefuncties van Microsoft 365 — REGEXTEST, REGEXEXTRACT en REGEXREPLACE — binnen zijn eigen Delphi-formule-engine, zonder Excel-installatie en zonder systeem-regex-DLL. De calculator van HotXLS linkt een vastgepinde 16-bits PCRE2 10.49-build statisch, matcht UTF-16-tekst rechtstreeks, broadcast over arrays en schrijft elke backtracking-stap toe op het resourcebudget van de formule

Dat laatste deel is de reden dat dit artikel bestaat. Een werkboek dat van een klant binnenkomt is onvertrouwde invoer, en een reguliere expressie in een formule is code die de klant schreef. Patroon plus tekst is genoeg om minutenlang een CPU-kern vast te zetten met catastrofale backtracking — de klassieke ReDoS-vorm (a+)+$ tegen een lange reeks a-tekens met een afsluitende b. Een server die geüploade werkboeken herberekent kan dat niet hebben, dus het interessante werk in HotXLS 2.384.112 was niet het aanroepen van PCRE2. Het was PCRE2 stoppbaar, verantwoordelijk en identiek op elk doelplatform maken

Wat geven REGEXTEST, REGEXEXTRACT en REGEXREPLACE in HotXLS terug?

De formule-engine van HotXLS volgt de gepubliceerde signatures van Microsoft, en de argumentsemantiek is wat u al uit Excel kent. Matchen is standaard hoofdlettergevoelig. Het laatste argument case_sensitivity neemt 0 voor hoofdlettergevoelig en 1 voor hoofdletteronafhankelijk, en elke andere waarde geeft #VALUE! terug. Inline PCRE2-opties zoals (?i) of (*UCP) werken ook, als u het gedrag liever binnen het patroon houdt

REGEXTEST(text, pattern, [case_sensitivity]) geeft een Boolean terug, en een mislukte match is simpelweg FALSE, nooit een fout. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) kent drie modi: 0 geeft de eerste complete match terug, 1 geeft elke complete match als verticale array, en 2 geeft de capture groups van de eerste match als horizontale array. Geen match geeft #N/A, modus 2 op een patroon zonder capture groups geeft #VALUE!, en een optionele groep die niet meedeed levert een lege string op in plaats van een fout

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) gebruikt PCRE2-replacement-syntaxis, dus $1 en ${name} verwijzen naar captures. Een occurrence van 0 vervangt elke match, een positieve waarde pakt de nde match vanaf het begin en een negatieve waarde telt vanaf het einde. Bestaat de gekozen occurrence niet, dan geeft HotXLS de originele tekst ongewijzigd terug. Numerieke selectors worden afgekapt richting nul, net zoals Excel gebroken integer-argumenten behandelt

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[2, 1].Value := 'PO-2026-00417 shipped to Graz';

    // TRUE wanneer de cel een inkoopordernummer bevat
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

    // Eerste complete match: "PO-2026-00417"
    Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';

    // Jaar en volgnummer wisselen met genummerde captures: "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Hoofdletteronafhankelijke test met het laatste argument
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

    Book.Recalculate;
    Book.SaveAs('orders.xlsx');
  finally
    Book.Free;
  end;
end;

Formuletekst in Delphi is een gewone string-literal, dus \d heeft aan de Pascalkant geen escaping nodig. Alleen de Excel-dubbele aanhalingstekens binnen de formule doen ertoe, en die volgen de normale Excel-regels. Bij het opslaan bewaart de XLSX-schrijver deze namen met het toekomstfunctie-prefix _xlfn. dat Excel verwacht, en een heropend werkboek rekent ze opnieuw uit. De klassieke BIFF8-engine kan de functies in een contextuele aanroep evalueren, maar kan ze niet als native .xls-formuletokens wegschrijven, dus houd regex-formules in XLSX-uitvoer

Hoe werkt array-broadcasting met regex-formules?

REGEXTEST, REGEXREPLACE en modus 0 van REGEXEXTRACT broadcasten over compatibele operanden, en dat is wat ze bruikbaar maakt op echte kolommen. Elk argument mag een scalar zijn, een rij, een kolom of een rechthoek, en HotXLS neemt het grootste rij- en kolomaantal als vorm van het resultaat. Een operand waarvan de dimensie noch 1 is noch dat maximum, laat de hele aanroep #VALUE! teruggeven — de gebruikelijke Excel-array-mismatch. Zo levert REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") 499 Booleans op, en werkt een kolom met patronen tegen één tekstcel op dezelfde manier in de andere richting

Modus 1 en 2 van REGEXEXTRACT weigeren te broadcasten. Elk element zou een vector van andere lengte opleveren, en een onsamenhangende set vectoren kan geen rechthoekig formuleresultaat vullen. In plaats van stilletjes af te kappen of op te vullen, meldt HotXLS de aanroep als niet-ondersteund. Heeft u alle matches uit veel cellen nodig, itereer dan over de cellen en extraheer er één per keer, of vorm het probleem om met LET- en LAMBDA-closures in de HotXLS-formule-engine zodat elke closure-aanroep één tekstwaarde ziet

De plaatsing van fouten binnen een broadcast-resultaat is bewust. Een Excel-fout voor één element — bijvoorbeeld #N/A van een extractie die in rij 37 niets vond — belandt in die ene cel van het resultaat terwijl de andere cellen hun waarden houden. Een resourcelimiet of een annulering is anders: die stopt de hele aanroep en publiceert nooit een halfgevulde array, want een half resultaat dat compleet oogt is erger dan een expliciete mislukking

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Opts: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Log');
    Sheet.Cells[1, 1].Value := 'GET /api/v2/orders 200 18ms';

    Opts := XLSDefaultFormulaEvaluationOptions;
    Opts.MaxEvaluationSteps := 200000;   // begrens het werk dat deze formule mag doen

    // Modus 2: capture groups van de eerste match als 1 x 3-array
    Eval := Sheet.EvaluateFormulaAt(1, 2,
      'REGEXEXTRACT(A1, "(GET|POST) (\S+) (\d{3})", 2)', xlfrsA1, Opts);

    case Eval.Status of
      xlfeOk:
        if VarIsArray(Eval.Value) then
          for I := VarArrayLowBound(Eval.Value, 2) to
                   VarArrayHighBound(Eval.Value, 2) do
            Log(VarToStr(Eval.Value[0, I]));
      xlfeResourceLimit:
        Log('Pattern exceeded its budget: ' + Eval.Issue.Message);
    else
      Log('Evaluation failed: ' + Eval.Issue.Message);
    end;
  finally
    Book.Free;
  end;
end;

TXLSXWorksheet.EvaluateFormulaAt is het juiste entrypoint wanneer het patroon ergens vandaan komt dat u niet beheert. Hij evalueert de formule alsof die op de gegeven rij en kolom leeft, geeft de volledige array terug in plaats van één gecachte celwaarde, en neemt TXLSFormulaEvaluationOptions met expliciete stap-, array-geheugen- en annuleringsregeling. De gids over de HotXLS-formule-engine en custom functions behandelt de rest van die evaluatie-API

Hoe stopt HotXLS een catastrofaal backtrackend patroon?

HotXLS stopt doorgeschoten patronen door PCRE2 bij elke stap terug te laten rapporteren aan de formulecontext. De backend compileert elk patroon met PCRE2_AUTO_CALLOUT, waardoor PCRE2 bij elk patroonitem tijdens het matchen een Pascal-callback aanroept. Die callback schrijft één werkeenheid toe aan de actieve evaluatiestatus, controleert de annuleringsvlag en controleert resident geheugen. Zegt een van die drie stop, dan geeft de callback PCRE2's callout-abort-code terug en wikkelt de match netjes af binnen PCRE2. De formule komt naar buiten als xlfeResourceLimit of een annuleringsstatus, niet als een hangende thread

Geheugen wordt op dezelfde manier gemeterd. Elke PCRE2-context wordt aangemaakt met een per-instantie general context waarvan de allocator een Pascal-functie is, dus het gecompileerde patroon, de matchdata en PCRE2's interne backtracking-frames worden allemaal via HotXLS gealloceerd. Elk blok voert een kleine header mee en is uitgelijnd op 16 bytes, en zijn grootte gaat in een lopend totaal dat de budgetcontrole toetst aan de array-geheugenlimiet van de formule. Uitvoerbuffers, extractievectoren en de replacement-string tellen mee in hetzelfde totaal, wat betekent dat een patroon dat miljoenen minieme strings matcht het plafond net zo zeker raakt als een patroon dat eeuwig backtrackt

De budget-callback draait bovendien onder een beveiliging die zwaarder telt dan hij lijkt. Een Delphi-exception die opgeworpen wordt binnen een callback die PCRE2 aanriep, mag nooit omhoog propageren, want tussen de callback en uw code zitten C-stackframes die gecompileerd zijn zonder enige kennis van Delphi-exceptiontabellen. Door ze heen unwinden is in het beste geval ongedefinieerd en in het slechtste geval een gecorrumpeerde stack. De HotXLS-bridge vangt elke exception in de callback af, zet de instantie in een aborted-status en geeft een gewone foutcode aan PCRE2 terug. Uw code ziet een mislukte formule, nooit een exception die door een vreemd frame heen brak

De kosten zijn echt en het vermelden waard. Auto callouts maken matchen trager dan een kale PCRE2-interpreter, en HotXLS compileert het patroon één keer per geëvalueerd element, dus een broadcast over 50.000 cellen betaalt 50.000 compilaties. Voor bulktekstwerk op vertrouwde data is een dedicated Delphi-regex-pas buiten het werkboek sneller. Binnen een werkboek dat vreemden u toesturen, wint begrensd en voorspelbaar het van snel

Waarom een statische UTF-16 PCRE2-build zonder JIT?

Excel-strings zijn UTF-16, en Delphi-WideString-waarden ook, waardoor de 16-bits PCRE2-bibliotheek de enige zinnige keuze was. HotXLS geeft formuletekst aan pcre2_match_16 met PCRE2_UTF gezet, en elke offset die terugkomt is een UTF-16-code-eenheid-index. Er vindt geen UTF-8-omweg plaats, dus niets kan afglijden op surrogate pairs. Waar een lege match de scan dwingt door te schuiven, volgt HotXLS de global-match-regel van PCRE2: probeer een niet-lege verankerde match op dezelfde positie opnieuw, en schuif daarna één Unicode scalar value door, wat twee code-eenheden voorbij een surrogate pair gaat in plaats van hem te splitsen

JIT staat met opzet uit. De JIT-compiler heeft uitvoerbaar geheugen en een eigen JIT-stack nodig, wat platformspecifieke code en allocaties buiten de per-formule-allocator oplevert. De interpreter houdt elke byte onder het HotXLS-budget, houdt elke match onder de callout en gedraagt zich op alle vijf de doelen hetzelfde. Elke TXLSRegex-instantie, de interne klasse achter deze formules, bezit zijn eigen contexts, dus parallelle rekenwerkers delen geen muteerbare PCRE2-status

Het linken is waar de build ongewoon werd. Delphi linkt C-objectbestanden met {$L}, en zijn linker herleidt symbolen tussen Pascal-units en C-objecten prima, maar hij herleidt geen willekeurige circulaire verwijzingen tussen aparte C-objectbestanden. PCRE2 is ruim twee dozijn translation units die elkaar alle kanten op aanroepen. De oplossing is een unity build: heel PCRE2 wordt als één translation unit gecompileerd tot één pcre2_unity-object per doel, zodat elke interne verwijzing binnen één enkel object oplost en de Delphi-linker alleen de geëxporteerde _16-functies ziet

De C-runtime was het tweede probleem. PCRE2 roept malloc, memcpy, memcmp en vrienden aan, en andere C-backends die in dezelfde executable linken, brengen mogelijk al hun eigen kopieën van die namen mee. HotXLS preprocesset PCRE2 zodat die aanroepen naar private namen gaan zoals hotxls_regex_memcpy, en implementeert ze in Pascal. Clang kan na het preprocessen nog gewone memset- en memchr-aanroepen synthetiseren, dus die twee bestaan er ook, binnen dezelfde unit. Symbooldecoratie moet per objectformaat matchen: het Win32 OMF-object verwacht de C-namen met voorloopunderscore, terwijl de Pascal-declaraties voor het Win64 ELF-object, het Win64 COFF-object en de FPC-builds de kale namen binden

De stackprobes waren het subtielste. Een C-functie met een frame groter dan een pagina roept eerst een probeeroutine aan, die elke pagina van 4 KB op volgorde aanraakt zodat Windows de stack via zijn guard page kan committen. Een stub die simpelweg terugkeert, linkt prima en werkt in elke unittest met een klein patroon, en crasht dan de eerste keer dat een diep frame voorbij de guard page springt. HotXLS implementeert echte probes in assembly per Windows-objectformaat: __chkstk_noalloc voor het Win32-object, __chkstk voor het Win64 ELF-object en ___chkstk_ms voor het Win64 COFF-object. Op Linux x64 en macOS ARM64 linkt FPC in plaats daarvan de systeem-C-bibliotheek, en elke andere FPC-doel meldt de backend als niet beschikbaar in plaats van stilletjes terug te vallen op een andere regex-dialect

Waar zitten de grenzen?

De regex-functies draaien op Delphi- en C++Builder-Windows-builds en op FPC Linux x64 en macOS ARM64. Op elk ander doel meldt de formule via contextuele evaluatie een niet-ondersteunde functie in plaats van met een andere engine andere antwoorden te geven. Dat is de juiste mislukking: een werkboek dat op een ander platform anders rekent, is een bug die niemand vindt tot een audit

Ook de Excel-equivalentie heeft een benoemde grens. De signatures en de gedocumenteerde voorbeelden volgen de REGEXTEST-, REGEXEXTRACT- en REGEXREPLACE-pagina's van Microsoft, maar de Excel-build die voor lokale validatie gebruikt wordt, stelt deze Microsoft 365-functies nog niet bloot. Randgevallen volgen daarom de expliciete PCRE2- en array-contracten hierboven in plaats van de claim dat elke Excel-build bit voor bit gematcht wordt. Hangen uw werkboeken af van een obscure hoek, zoals specifiek backreference-gedrag of een Unicode property class, test die hoek dan rechtstreeks

FAQ

Heeft HotXLS Excel of een regex-DLL nodig om REGEXTEST te berekenen?

Nee. HotXLS linkt een statisch UTF-16 PCRE2-object in uw executable en evalueert REGEXTEST, REGEXEXTRACT en REGEXREPLACE in zijn eigen Delphi-formule-engine, zonder Excel, COM of externe DLL

Kan een kwaadaardig patroon mijn server laten hangen?

Niet voorbij het ingestelde budget. Elke matchstap gaat door een callout die de formulecontext belast, dus een ReDoS-patroon eindigt met xlfeResourceLimit of een annuleringsstatus en zonder gedeeltelijk resultaat

Waarom faalt REGEXEXTRACT-modus 1 op een bereikargument?

Elke cel zou een ander aantal matches kunnen opleveren, en die onsamenhangende vectoren kunnen geen rechthoekig resultaat vormen. HotXLS meldt de aanroep als niet-ondersteund in plaats van op te vullen of af te kappen, dus extraheer één cel per keer

HotXLS levert de regex-functies naast LAMBDA, GROUPBY, PIVOTBY en de rest van zijn rekenengine voor Delphi, C++Builder en Lazarus. Details, edities en de proefdownload staan op de HotXLS Delphi spreadsheet component-pagina