Tällainen ongelma ilmestyy heti, kun PDF-kirjasto siirtyy kotikielensä ulkopuolelle. Sinulla on sidos, joka toimii täydellisesti C#:sta Windowsissa. Tarvitset samat kutsut Pythonista macOS:ssä, joten kopioit Windowsin deklarointitiedoston, vaihdat binäärin nimen ja suoritat sen. Jokainen symboli ratkeaa. Ensimmäinen kutsu palauttaa roskaa, toinen kaatuu käyttöoikeusrikkomukseen, eikä PDF-koodisi ole muuttunut. Vika on PDF-tason alapuolella: Windows-viennit käyttävät Stdcall-kutsukäytäntöä, macOS:n dylib vie samat funktiot Cdecl-käytäntönä alaviiva nimen edessä, ja vieraan funktion deklarointi, jossa kumpikin yksityiskohta on väärin, turmelee pinon ennen kuin yhtäkään asiakirjaa avataan
Koko tämä virheluokka johtuu yhdestä suunnittelupäätöksestä, joka kannattaa ymmärtää heti alussa. PDF Library for Delphi, losLabin lähdekoodiltaan saatavilla oleva PDF-moottori Delphille ja C++Builderille, käärii koko oliomallinsa yhteen litteään julkisivuluokkaan, TPDFlib, ja toimittaa sen sitten kolmessa binäärimuodossa: Windowsin DLL:nä, COM/ActiveX-automaatio-objektina ja macOS:n dylib-kirjastona. PDF-semanttiikka on kaikissa kolmessa sama. Hankaluus piilee niiden alapuolella olevassa ABI:ssa: kutsukäytännöissä, merkkijonokoodauksissa, kahvojen omistajuudessa ja siinä, kumman osapuolen on sallittua vapauttaa mikäkin puskuri
Yksi julkisivu, kolme binäärimuotoa
Jokaisella TPDFlib-luokan julkisella funktiolla on litteä vastine, jonka nimenä on DL ja metodin nimi. LoadFromFile-metodista tulee DLLoadFromFile, Encrypt-metodista DLEncrypt ja NewSignProcessFromFile-metodista DLNewSignProcessFromFile. Lähes jokaisen viennin ensimmäinen parametri on InstanceID, jonka DLCreateLibrary palauttaa ja joka korvaa olioviittauksen, jota Delphi-kutsuja muuten pitäisi hallussaan. Sisäistä tämä kartoitus aikaisin. Se tarkoittaa, että Delphin API-viite toimii samalla dokumentaationa kaikille muille kielille: mitä luokka pystyy tekemään, DLL pystyy tekemään ennustettavalla nimellä, ja Pascal-metodin allekirjoitus kertoo, millaisen kutsun tarvitset Pythonista tai C#:sta
Windows-versio tuottaa tiedostot PDFlibDLL32.dll ja PDFlibDLL64.dll; valitse isäntäprosessisi bittisyyttä vastaava tiedosto, sillä 64-bittinen Java- tai .NET-prosessi ei voi ladata 32-bittistä kirjastoa riippumatta siitä, miltä deklarointi näyttää
Windows: Stdcall-instanssit ja W/A-funktioparit
Jokainen merkkijonoja vastaanottava vienti on olemassa kahdesti. Leveä versio vastaanottaa PWideChar-arvon (UTF-16, joka sopii luontevasti .NET:iin, Javaan ja Pythonin c_wchar_p-tyyppiin), ja A-päätteinen versio vastaanottaa PAnsiChar-arvon. Niiden semantiikka on sama ja ero on vain koodauksessa, mikä tekee niiden sekoittamisesta niin hankalaa jäljittää: mikään ei heitä poikkeusta, mikään ei palauta virhekoodia, vaan metatietoihin ilmestyy merkkisotkua tai polusta, jossa on tavallisen ASCII-alueen ulkopuolinen merkki, saadaan aiheeton "tiedostoa ei löydy" -virhe. Ensimmäinen tällä tavoin kohdattu koodausvirhe vie tiimiltä yleensä iltapäivän, koska oire viittaa dataan ja syy on deklaroinnissa
// Windows-sidos (PDFlibDLL64.dll): Stdcall, suorat vientinimet
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// macOS-sidos: sama funktio, Cdecl ja alaviite vientinimen etuliitteenä
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Valitse isäntää kohden yksi merkkileveys ja tee siitä sääntö sidoksen luontityökaluun. Käytännöllinen sääntö on tämä: jos isäntäkielellä on natiivit UTF-16-merkkijonot, sido W-versiot kaikkialle äläkä enää käytä A-perhettä
macOS: samat nimet, eri ABI
Dylib vie saman DL-funktiojoukon kahdella järjestelmällisellä muutoksella. Kutsukäytäntö on Stdcallin sijaan Cdecl, ja jokaisen vientinimen edessä on alaviiva (_DLCreateLibrary, _DLLoadFromFile ja niin edelleen). Molemmat muutokset ovat puhtaasti mekaanisia, mikä tekee niistä ihanteellisia generoidulle sidokselle ja vaarallisia käsin muokatulle Windows-tiedoston kopiolle. Pidä yksi kanoninen funktioluettelo ja tuota siitä alustakohtaiset deklaroinnit, jos työkalusi sallivat sen. Jos jätät tämän tekemättä, saat sivun alussa kuvatun pinon turmeltumisen, joka toistuu vain alustalla, jota CI:si sattuu testaamaan vähiten
COM- ja ActiveX-isännät: Safecall ja Olevariant-kuormat
VB.NET:lle, C#:lle, VBSciptille ja vanhoille automaatioisännille OCX-versio käärii saman julkisivun IDispatch-automaatio-objektiin, IPDFlibrary, jossa jokainen metodi on deklaroitu Safecalliksi. Tämä käytäntö muuttaa virheiden välittymistavan. Safecall muuntaa sisäisen virheen COM-HRESULT-arvoksi, joten C#-kutsuja saa poikkeuksen tilanteessa, jossa litteä DLL palauttaisi huomaamattoman kokonaisluvun, jonka tarkistaminen kutsujan pitäisi muistaa. Sama toiminto, kaksi virheilmaisutapaa sen mukaan, minkä binäärin latasit
Binääridata noudattaa toista COM-kohtaista sääntöä. Automaatiorajapinnassa ei ole lainkaan osoitinparametreja. Kaikki binäärinen, sisään tulevat kuvabitit tai ulos tulevat PDF-tavut, ylittää rajan Olevariant-arvona metodien kuten AddImageFromVariant ja AppendToVariant kautta. Tavutaulukon marshalaus variantiksi on .NET:ssä yksi rivi. Jos yrität antaa sille raa'an osoittimen sillä perusteella, että kyseessä on kuitenkin sama prosessi, dispatch-kerros hylkää tai sotkee kutsun. Vielä yksi rekisteröintiyksityiskohta aiheuttaa ongelmia käyttöönotossa: COM-rekisteröinti on bittisyyskohtaista, joten 32-bittisellä regsvr32-ohjelmalla rekisteröity OCX on näkymätön 64-bittiselle isännälle. Tämä ristiriita näkyy asiakkaan koneessa surullisen hyödyttömänä "luokkaa ei ole rekisteröity" -virheenä kauan sen jälkeen, kun ohjelma on lähtenyt sinun koneeltasi
Kahvakuri: instanssit omistavat asiakirjat
Litteä API käyttää kokonaislukukahvoja. DLCreateLibrary palauttaa instanssin. Tiedoston lataaminen palauttaa kyseisen instanssin sisäisen asiakirjatunnuksen. Allekirjoitusprosessit, merkkijonoluettelot ja suoratiedostot palauttavat kukin omat kokonaislukukahvansa, jotka kaikki kuuluvat samaan instanssiin. Elinkaari näyttää samalta kaikista FFI-isännistä katsottuna; tässä se esitetään Pascalilla, koska se on selkeälukuinen:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // yksi instanssi worker-säiettä kohti
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returns a DocumentID, 0 on failure
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // vapauttaa kaikki instanssin omistamat asiakirjat
end;
end;
Omistajuuspuusta seuraa kaksi asiaa. DLReleaseLibrary on ainoa välttämättä tarvitsemasi siivouskutsu, koska se purkaa yhdellä kertaa jokaisen instanssin alla olevan asiakirja- ja prosessikahvan. Lyhyessä skriptissä se riittää. Pitkäkestoisessa palvelussa siitä tulee hidas vuoto ylimääräisellä seremonialla, joten vapauta asiakirjat, kun olet saanut ne käsiteltyä, äläkä anna niiden kasaantua instanssin päättymiseen asti. Instanssi on myös luonteva säie-eristyksen yksikkö. Anna jokaiselle työsäikeelle oma InstanceID äläkä koskaan jaa yhtä säikeiden välillä ilman ulkoista lukitusta, samasta syystä kuin et jakaisi yhtä TPDFlib-oliota säikeiden kesken
Palautetut merkkijonot ovat lainattuja, eivät omistettuja
Tekstiä palauttavat funktiot, kuten DLGetPageText, antavat takaisin PWideChar- tai PAnsiChar-osoittimen, joka osoittaa kirjastoinstanssin omistamaan ja uudelleenkäyttämään puskuriin. Sopimus on: kopioi heti, älä koskaan vapauta
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // kopioi nyt; myöhempi kutsu voi käyttää puskuria uudelleen
end;
C#:ssa tämä tarkoittaa IntPtr-arvon marshalaamista hallituksi merkkijonoksi ennen seuraavaa kirjastokutsua. Pythonin ctypesissa se tarkoittaa leveän merkkijonon leikkaamista osoittimesta heti. Jos pidät raa'an osoittimen kutsujen yli, olet kirjoittanut virheen, joka läpäisee kaikki yksikkötestit ja epäonnistuu sitten ensimmäisellä kerralla, kun kaksi pyyntöä menee päällekkäin tuotannossa, koska toinen kutsu kierrätti puskurin, jota ensimmäinen vielä luki. Sama omistussääntö toimii toiseen suuntaan DLSetProgressCallback-kutsulla rekisteröidyissä takaisinkutsuissa. Kaikki osoittimet, jotka kirjasto antaa takaisinkutsullesi, ovat voimassa vain kyseisen takaisinkutsun rungon ajan, ja itse takaisinkutsuobjektin on pysyttävä elossa (roskien keräämää isäntää käytettäessä kiinnitettynä) niin kauan kuin instanssi voi vielä kutsua sitä. Työn aikana kerätty delegaatti on oppikirjaesimerkki "satunnaisesta" käyttöoikeusrikkomuksesta, joka ilmestyy .NET-sidokseen, joka toimi puhtaasti kuukausien ajan
Rakenna sidokseen oma savutesti ja suorita se ennen kuin yksikään generoitu deklarointijoukko toimitetaan. Kokeile yhtä kutsua jokaisesta luokasta, joka yleensä paljastaa ABI-virheitä: parametritonta funktiota, kuten DLCreateLibrary, jotta näet kutsukäytännön olevan oikea, merkkijonoa vastaanottavaa funktiota polulla, jossa on muita kuin ASCII-merkkejä, jotta näet koodauksen olevan oikea, merkkijonoa palauttavaa funktiota, jotta näet lainatun puskurin käsittelyn olevan oikea, ja yhtä tarkoituksella epäonnistuvaa toimintoa, jotta voit tarkkailla, miten virhe saavuttaa isäntäsi. Se on vartin työ, ja se löytää kutsukäytäntö- ja koodausvirheet, jotka muuten tulisivat kuukausia myöhemmin asiakasympäristöstä kaatumisvedoksena
Python ctypes -tapaus käytännössä
Python ctypes on sidos, jonka näen käsin tehdyn useimmin, ja sen avulla alustojen välinen ero on helppo havainnollistaa. Lataa Windowsissa kirjasto ctypes.WinDLL-luokalla, jotta ctypes käyttää Stdcallia, sido päätteettömät W-funktiot ja deklaroi jokainen merkkijonoparametri c_wchar_p-tyyppiseksi. Lataa macOS:ssä se ctypes.CDLL-luokalla Cdecliä varten, pidä funktioluettelo muuten samana ja ratkaise nimet ilman alussa olevaa alaviivaa. Useimmat FFI-kerrokset, ctypes mukaan lukien, palauttavat alaviivakäytännön puolestasi macOS:ssä, mutta tämä on se oletus, joka pitää vahvistaa yhdellä ratkenneella kutsulla ennen kuin sen päälle generoidaan satoja deklarointeja
Sidoskehitystä seuraa kaksi käyttöönottoa koskevaa kysymystä, joihin on selkeät vastaukset. Tavallinen DLL ei tarvitse rekisteröintiä: regsvr32 koskee vain ActiveX-versiota, ja DLL toimitetaan tiedostokopiointina, minkä vuoksi se on ensisijainen valinta Windows-palveluille ja säilöille, joissa rekisteriin ei haluta koskea lainkaan. Säieturvallisuus palautuu jo edellä käytössä olleeseen sääntöön: yksi instanssi per säie. Instanssikahva sisältää kaiken moottorin seuraaman muuttuvan tilan, valitun asiakirjan, renderöintiasetukset ja purkuasetukset, joten kaksi instanssin jakavaa säiettä lomittaa toistensa tilan silloinkin, kun jokainen yksittäinen kutsu palauttaa onnistumisen
Kun sidos on vakaa, sen toisella puolella olevat toiminnot ovat juuri niitä, joita Delphi-artikkelit käsittelevät perusteellisesti, mukaan lukien PDF-salauksen käyttöönotto ja auditointi sekä tekstin ja kuvien purkaminen olemassa olevista asiakirjoista
Kaikkien kolmen integraatiotason binäärilataukset toimitetaan kirjaston mukana; katso versiot ja lisensointi PDF Library for Delphi-tuotesivulta