Tekninen artikkeli

VBA-lähdekoodin uudelleenkirjoitus ja MS-OVBA-uudelleenpakkaus Delphissä

Kovakoodatun työarkkiviittauksen uudelleennimeäminen tuhannessa makrollisessa raporttimallissa sulkee pois jokaisen tiedoston avaamisen VBA-editorissa käsin. HotXLS, natiivi Delphi- ja C++Builder-Excel-komponentti, käsittelee tämän tapauksen paljastamalla VBA-moduulin lähdekoodin muokattavana SourceCode-ominaisuutena ja pakkaamalla jokaisen muokkauksen uudelleen MS-OVBA-pakkausalgoritmilla, jonka Microsoft määrittelee VBA-tallennukselle, kirjoittaen tuloksen takaisin klassiseen XLS VBA -tallennukseen, itsenäiseen VBA-projektitiedostoon tai makrollisiin XLSM-työkirjoihin. Missään tuolla polulla ei ole mukana Excel-instanssia, VBA-editoria eikä makronauhoitinta

Miksi VBA-moduulivirta ei ole tekstitiedosto

VBA-moduuli XLS-työkirjan tai itsenäisen VBA-projektitiedoston sisällä ei ole lähdetekstiä, joka istuu virrassa luettavaksi odottamassa — se on pieni binäärisäiliö. Käännetty suorituskykyvälimuisti tulee ensin, tavut, joita Office käyttää ohittaakseen moduulin uudelleenkäännön latauksessa, kun välimuisti yhä täsmää isäntäversion kanssa, ja todellinen lähdeteksti seuraa, ajettuna omistetun pakkausjärjestelmän läpi, jonka MS-OVBA määrittelee nimenomaan VBA-tallennukselle. Tuo järjestelmä ei ole zip, ei deflate, eikä mitään, mitä Windowsin pakkaus-API:t tuottavat natiivisti, mikä on juuri se, miksi useimmat kolmannen osapuolen Excel-kirjastot pystyvät lukemaan moduulin lähdekoodin — purkaminen on ongelman helpompi puoli — mutta jäävät sen kirjoittamisesta takaisin, koska uudelleenpakkaus on paikka, jossa hienovaraisesti väärä bitti tuottaa tiedoston, jonka Excel kieltäytyy avaamasta. Julkisia kirjoituksia lukupuolesta on olemassa; kirjoituspuolen toteutuksia, jotka todella harjoittavat uudelleenpakkausta, eivätkä vain pura olemassa olevaa moduulia tarkastelua varten, on riittävän vähän, että tämä pysyy yhtenä Excel-tiedostomuotojen vähiten dokumentoiduista nurkista

Mitä HotXLS:n SourceCode-ominaisuus todella muuttaa?

HotXLS esittää jokaisen VBA-moduulin TXLSVBAModule-oliona, jolla on tavallinen SourceCode: WideString-ominaisuus, ja sen asettaminen uuteen arvoon on täsmälleen yhtä yksinkertaista kuin miltä se näyttää: moduuli merkitään muistissa likaiseksi, eikä mikään kosketa taustalla olevaa OLE-virtaa ennen kuin projekti tallennetaan. Projekti itse tulee IXLSWorkbook.VBAProject-ominaisuudesta klassisessa XLS-moottorissa tai TXLSXWorkbook.ParsedVBAProject-ominaisuudesta OOXML-makromoottorissa, molemmat palauttaen TXLSVBAProject-olion, jonka moduulit istuvat 1-pohjaisen Item[]-indeksoijan ja Count-ominaisuuden takana, joten eräkohtainen muokkaus jokaisen työkirjan moduulin yli on vain silmukka kokonaislukualueen yli

var
  Wb: TXLSWorkbook;
  Project: TXLSVBAProject;
  I: Integer;
  Updated: WideString;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Open('MonthlyReport.xls');
    if Wb.HasVBAProject then
    begin
      Project := Wb.VBAProject;
      for I := 1 to Project.Count do
      begin
        Updated := StringReplace(Project[I].SourceCode,
          'ReportSheet2025', 'ReportSheet2026', [rfReplaceAll]);
        if Updated <> Project[I].SourceCode then
          Project[I].SourceCode := Updated;   // marks the module dirty
      end;
      Wb.SaveAs('MonthlyReport.xls');          // recompresses on write
    end;
  finally
    Wb.Free;
  end;
end;

Tuo silmukka on myös tarkastuskierroksen muoto. Ennen kuin tuhatta mallia kosketetaan, useimmat tiimit haluavat ensin tietää, kuinka moni niistä todella kantaa makroja ja mihin nuo makrot viittaavat, mikä on skenaario työkirjan tarkastus- ja muunnostyöpenkin takana — sama Project.Count, joka ohjaa uudelleenkirjoitussilmukkaa tässä, muuttuu tiedostokohtaiseksi makrolaskuriksi siellä

MS-OVBA-pakkaussäiliön sisällä

MS-OVBA:n pakkausmuoto paketoi lähdetavut siksi, mitä spesifikaatio kutsuu CompressedContaineriksi: yksi allekirjoitustavu, jonka on oltava 0x01, jota seuraa sarja CompressedChunk-lohkoja, joista kukin kattaa enintään 4096 tavua purettua dataa. 16-bittinen lohko-otsikko kantaa kolme kenttää — 3-bittisen allekirjoituksen, jonka on oltava 3, 12-bittisen kokokentän ja CompressedChunkFlag-bitin, joka merkitsee, onko lohkon hyötykuorma kirjaimellisia tavuja vai tunniste-pakattu sekvenssi. Kun lippu on asetettu, hyötykuorma on sarja lippitavulla varustettuja kahdeksan tunnisteen ryhmiä, ja jokainen tunniste on joko yksi kirjaimellinen tavu tai CopyToken: siirtymä/pituus-takaisinviittaus tavuihin, jotka on jo purettu aiemmin samassa lohkossa, bittileveyden jakautuessa siirtymän ja pituuden välillä sen mukaan, kuinka pitkälle lohkoon purkaja tällä hetkellä on edennyt. Tämä osa MS-OVBA:sta (§2.4.1, Compression and Decompression) on paikka, jossa käsin rakennettu toteutus useimmin menettää päivän tuon bittileveyslaskennan yhden verran väärin -virheeseen

Miksi HotXLS kirjoittaa raakoja lohkoja tunnisteiden täsmäyttämisen sijaan

HotXLS:n kirjoituspolku ohittaa tuon algoritmin tunnisteiden täsmäytyspuolen kokonaan. Kun se pakkaa muokatun moduulin uudelleen, jokainen lohko lähtee ulos CompressedChunkFlag-lippu tyhjennettynä, mikä tarkoittaa, että lohko kantaa kirjaimellisia tavuja takaisinviittaustunnisteiden sijaan — laillista MS-OVBA:n alla, koska pakattu säiliö saa koostua kokonaan pakkaamattomista lohkoista, ja se poistaa juuri sen algoritmin osan, joka on vaikein saada oikein käsin: pätevien takaisinviittausten löytäminen ja siirtymä/pituus-parin pakkaaminen bittileveyteen, joka riippuu nykyisestä sijainnista lohkon sisällä. Kompromissi näkyy tiedostokoossa, ei oikeellisuudessa — uudelleenkirjoitettu moduulivirta päätyy lähelle lähdetekstinsä kokoa plus kahden tavun otsikko per 4096-tavuinen lohko, ei pienemmäksi, kuten täysin tunniste-pakattu lohko olisi. Jokainen lukija, joka toteuttaa spesifikaation purkupuolen, Excel mukaan lukien, avaa silti tuloksen oikein, koska raaka lohko on yhtä pätevä CompressedChunk kuin tunniste-pakattu

Mitä HotXLS jättää koskemattomaksi, kun se kirjoittaa moduulin uudelleen

Uudelleenpakkaus korvaa vain koskaan osan moduulivirrasta. Jokainen moduulivirta tallentaa suorituskykyvälimuistinsa ensin ja pakatun lähteensä toiseksi, ja projektin dir-virta tallentaa täsmälleen, mihin tuo jako osuu jokaiselle moduulille MODULEOFFSET-merkinnässä; HotXLS lukee tuon siirtymän, pitää jokaisen sitä edeltävän tavun täsmälleen sellaisena kuin se löysi ne, ja rakentaa uudelleen vain pakatun säiliön siirtymästä eteenpäin

Itse lähdeteksti kulkee edestakaisin VBA-projektin oman koodisivun kautta UTF-8:n sijaan — saman vanhan koodisivun, jolla Office kirjoitti projektin alun perin. SourceCode-muokkaus, joka tuo mukanaan merkkejä tuon koodisivun repertuaarin ulkopuolelta, korvataan hiljaa parhaiten sopivilla korvausmerkeillä, kun HotXLS koodaa merkkijonon takaisin tavuiksi, eikä sitä hylätä, joten epätavallinen alueellinen merkki, joka on pudotettu kommenttiin tai merkkijonoliteraaliin, on todennäköisin paikka huomata menetys. Ulkoiset viittaukset ja kirjastosidonnat saman projektin sisällä noudattavat toisiinsa liittyvää mutta erillistä säilytyspolkua, joka käsitellään artikkelissa rinnakkaisartikkeli VBA:n ulkoisten linkkien säilyttämisestä, ja se kannattaa lukea ennen kuin uudelleenkirjoituskierros koskettaa projektia, joka linkittyy ulos muihin työkirjoihin tai tyyppikirjastoihin

Miten saat uudelleenkirjoitetut makrot takaisin työkirjaan?

Mikään ei kutsu uudelleenpakkausvaihetta nimenomaisesti — se ajetaan automaattisesti heti, kun työkirja tai itsenäinen VBA-projekti tallennetaan. TXLSVBAProject.ApplyChanges käy läpi jokaisen moduulin, pakkaa uudelleen ne, joiden SourceCode muuttui viimeisimmän tallennuksen jälkeen, ja kirjoittaa uudelleen vain kyseisen moduulin virran; klassinen TXLSWorkbook.SaveAs, kun tallennuskohde pitää tiedoston alkuperäisen muodon, ja OOXML TXLSXWorkbook.SaveAs makrolliselle XLSM-paketille kutsuvat sitä molemmat sisäisesti ennen kuin mitään kirjoitetaan levylle, ja SaveVBAProjectToFile kutsuu samaa metodia, kun kohde on irrallinen VBA-projektitiedosto koko työkirjan sijaan

var
  Wb: TXLSWorkbook;
begin
  Wb := TXLSWorkbook.Create;
  try
    if Wb.LoadVBAProjectFromFile('LegacyMacros.ole') = 1 then
    begin
      Wb.VBAProject[1].SourceCode :=
        StringReplace(Wb.VBAProject[1].SourceCode, 'OldServer', 'NewServer', [rfReplaceAll]);
      Wb.SaveVBAProjectToFile('LegacyMacros_Patched.ole');  // ApplyChanges runs internally
    end;
  finally
    Wb.Free;
  end;
end;
var
  Xlsx: TXLSXWorkbook;
  Project: TXLSVBAProject;
begin
  Xlsx := TXLSXWorkbook.Create;
  try
    Xlsx.Open('Dashboard.xlsm');
    Project := Xlsx.ParsedVBAProject;
    if Assigned(Project) then
    begin
      Project[1].SourceCode := StringReplace(Project[1].SourceCode,
        'ConnStringV1', 'ConnStringV2', [rfReplaceAll]);
      Xlsx.SaveAs('Dashboard.xlsm');   // SyncParsedVBAProject recompresses before the part is written
    end;
  finally
    Xlsx.Free;
  end;
end;

Kaikki kolme kohdetta jakavat saman SourceCode- ja ApplyChanges-mekaniikan taustalla; ainoa todellinen ero niiden välillä on se, mikä tallennuskutsu lopulta laukaisee uudelleenpakkauksen

Missä tämä yhä hajoaa

Kaksi vikatilaa ovat riittävän yleisiä varautua ennen kuin uudelleenkirjoituskierros ajetaan tuotantotiedostoja vasten. Digitaalisesti allekirjoitettu VBA-projekti lakkaa olemasta pätevästi allekirjoitettu heti, kun sen lähde muuttuu, koska allekirjoitus kattaa projektin sisällön; HotXLS:llä ei ole tapaa allekirjoittaa projektia uudelleen puolestasi, ja Excel pudottaa tai merkitsee allekirjoituksen seuraavan kerran, kun tiedosto avataan, joten allekirjoitettu makroprojekti tarvitsee uudelleenallekirjoitusvaiheen alavirtaan, jos tuo allekirjoitus on jotain, mitä työnkulkusi todella tarkistaa. Toinen vikatila kuuluu kenelle tahansa, joka houkuttelee toteuttamaan tämän pakkausmuodon uudelleen alusta sen sijaan, että käyttäisi kirjastoa, joka jo käsittelee sen: yksi väärä bitti lohko-otsikossa, allekirjoitusnibblessä, kokokentässä tai pakattulipussa, tuottaa tiedoston, jonka Excel kieltäytyy avaamasta, yleensä geneerisen vioittumisvaroituksen takana, joka ei anna vihjettä siitä, mikä tavu oli väärä — juuri se bugiluokka, jonka välttämiseksi aiemmin kuvattu raakalohkokirjoitusstrategia on olemassa

Mikään tästä ei vaadi muodon takaisinmallinnusta käyttääksesi sitä. Delphi- ja C++Builder-kehittäjät saavat SourceCode-luku- ja kirjoitusoikeuden, MS-OVBA-yhteensopivan uudelleenpakkauksen ja kaikki kolme takaisinkirjoituskohdetta, jotka tässä kuvataan, osana vakiomuotoista HotXLS-komponenttia, yhdessä sen muun klassisen XLS- ja OOXML-työkirja-API:n kanssa