Perché utilizzare un "handle" opaco che richiede il cast in un'API pubblica anziché in un puntatore di struttura typesafe?


27

Sto valutando una libreria la cui API pubblica è attualmente simile a questa:

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

Si noti che enhè un handle generico, utilizzato come handle per diversi tipi di dati ( motori e hook ).

Internamente, la maggior parte di queste API ovviamente lancia il "handle" su una struttura interna che hanno malloc:

engine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

Personalmente, odio nascondere le cose dietro di typedefs, specialmente quando compromette la sicurezza dei tipi. (Dato un enh, come faccio a sapere a cosa si riferisce effettivamente?)

Quindi ho inviato una richiesta pull, suggerendo la seguente modifica API (dopo aver modificato l' intera libreria per renderla conforme):

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

Naturalmente, questo rende le implementazioni API interne molto migliori, eliminando i cast e mantenendo la sicurezza dei tipi dalla / alla prospettiva del consumatore.

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

Preferisco questo per i seguenti motivi:

Tuttavia, il proprietario del progetto si è opposto alla richiesta pull (parafrasato):

Personalmente non mi piace l'idea di esporre il struct engine. Penso ancora che il modo attuale sia più pulito e più amichevole.

Inizialmente ho usato un altro tipo di dati per l'handle hook, ma poi ho deciso di passare all'uso enh, quindi tutti i tipi di handle condividono lo stesso tipo di dati per mantenerlo semplice. Se questo è confuso, possiamo certamente usare un altro tipo di dati.

Vediamo cosa ne pensano gli altri di questo PR.

Questa libreria è attualmente in una fase beta privata, quindi non c'è molto codice di cui preoccuparsi (ancora). Inoltre, ho offuscato un po 'i nomi.


In che modo una maniglia opaca è migliore di una struttura opaca con nome?

Nota: ho posto questa domanda a Code Review , dove era chiusa.


1
Ho modificato il titolo in qualcosa che credo esprima più chiaramente il nocciolo della tua domanda. Sentiti libero di tornare se l'ho interpretato male.
Ixrec,

1
@Ixrec È meglio, grazie. Dopo aver scritto l'intera domanda, ho esaurito la capacità mentale di trovare un buon titolo.
Jonathon Reinhart,

Risposte:


33

Il mantra "semplice è meglio" è diventato troppo dogmatico. Semplice non è sempre meglio se complica altre cose. Assembly è semplice - ogni comando è molto più semplice dei comandi di lingue di livello superiore - e tuttavia i programmi di assembly sono più complessi delle lingue di livello superiore che fanno la stessa cosa. Nel tuo caso, il tipo di maniglia uniforme enhrende i tipi più semplici al costo di rendere complesse le funzioni. Dato che di solito i tipi di progetto tendono a crescere in una velocità sub-lineare rispetto alle sue funzioni, poiché il progetto diventa più grande di solito preferisci tipi più complessi se può rendere le funzioni più semplici - quindi a questo proposito il tuo approccio sembra essere quello corretto.

L'autore del progetto è preoccupato che il tuo approccio stia " esponendostruct engine ". Avrei spiegato loro che non sta esponendo la struttura stessa, ma solo il fatto che esiste una struttura chiamata engine. L'utente della libreria deve già essere a conoscenza di quel tipo - devono sapere, ad esempio, che il primo argomento di en_add_hookè di quel tipo e il primo argomento è di un tipo diverso. Quindi in realtà rende l'API più complessa, perché invece di avere la "firma" della funzione documenta questi tipi, deve essere documentata altrove e perché il compilatore non può più verificare i tipi per il programmatore.

Una cosa da notare: la tua nuova API rende il codice utente un po 'più complesso, poiché invece di scrivere:

enh en;
en_open(ENGINE_MODE_1, &en);

Ora hanno bisogno di una sintassi più complessa per dichiarare il loro handle:

struct engine* en;
en_open(ENGINE_MODE_1, &en);

La soluzione, tuttavia, è abbastanza semplice:

struct _engine;
typedef struct _engine* engine

e ora puoi scrivere direttamente:

engine en;
en_open(ENGINE_MODE_1, &en);

Ho dimenticato di dire che la biblioteca afferma di seguire lo stile di codifica Linux , che è anche quello che seguo. Lì, vedrai che le strutture di dattilografia solo per evitare la scrittura structsono espressamente scoraggiate.
Jonathon Reinhart,

@JonathonReinhart sta digitando il puntatore per strutturare non la struttura stessa.
maniaco del cricchetto,

@JonathonReinhart e in realtà leggendo quel link vedo che per "oggetti totalmente opachi" è permesso. (capitolo 5 regola a)
maniaco del cricchetto,

Sì, ma solo in casi eccezionalmente rari. Sinceramente credo che sia stato aggiunto per evitare di riscrivere tutto il codice mm per gestire i typedef pte. Guarda il codice blocco spin. È completamente specifico per arco (nessun dato comune) ma non usa mai un typedef.
Jonathon Reinhart,

8
Preferirei typedef struct engine engine;e userei engine*: introdotto un nome in meno, e questo rende ovvio che è una maniglia simile FILE*.
Deduplicatore,

16

Sembra esserci una confusione da entrambe le parti qui:

  • l'utilizzo di un approccio handle non richiede l'utilizzo di un tipo di handle singolo per tutti gli handle
  • esporre il structnome non espone i suoi dettagli (solo la sua esistenza)

Ci sono vantaggi nell'usare gli handle piuttosto che i puntatori nudi, in un linguaggio come C, perché la consegna del puntatore consente la manipolazione diretta del puntatore (comprese le chiamate a free) mentre la consegna di un handle richiede che il client passi attraverso l'API per eseguire qualsiasi azione .

Tuttavia, l'approccio di avere un solo tipo di handle, definito tramite a, typedefnon è sicuro e può causare molti problemi.

Il mio suggerimento personale sarebbe quindi quello di spostarmi verso maniglie di tipo sicuro, che penso soddisfarrebbero entrambi. Questo si ottiene piuttosto semplicemente:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

Ora, non si può passare accidentalmente 2come una maniglia, né si può passare accidentalmente una maniglia a un manico di scopa dove è prevista una maniglia per il motore.


Quindi ho inviato una richiesta pull, suggerendo la seguente modifica API (dopo aver modificato l' intera libreria per renderla conforme)

Questo è il tuo errore: prima di impegnarti in un lavoro significativo su una libreria open source, contatta gli autori / manutentori per discutere in anticipo della modifica . Permetterà a entrambi di essere d'accordo su cosa fare (o non fare) ed evitare il lavoro non necessario e la frustrazione che ne deriva.


1
Grazie. Non hai pensato a cosa fare con le maniglie. Ho implementato una vera API basata su handle , in cui i puntatori non vengono mai esposti, anche se tramite un typedef. Si trattava di un ~ ricerca costosa dei dati all'entrata di ogni chiamata API - molto simile al modo in cui Linux cerca la struct fileda un int fd. Questo è certamente eccessivo per una libreria IMO in modalità utente.
Jonathon Reinhart,

@JonathonReinhart: Beh, dato che la libreria fornisce già handle, non ho sentito il bisogno di espandersi. In effetti ci sono più approcci, dal semplice convertire il puntatore al numero intero per avere un "pool" e usare gli ID come chiavi. Puoi anche cambiare approccio tra Debug (ID + ricerca, per la validazione) e Release (solo puntatore convertito, per la velocità).
Matthieu M.,

Il riutilizzo dell'indice di tabella di numeri interi risentirà effettivamente del problema ABA , in cui un oggetto (indice 3) viene liberato, quindi viene creato un nuovo oggetto e purtroppo viene 3nuovamente assegnato all'indice . In poche parole, è difficile avere un meccanismo di durata degli oggetti sicuro in C a meno che il conteggio dei riferimenti (insieme alle convenzioni sulle proprietà condivise agli oggetti) sia trasformato in una parte esplicita del design dell'API.
rwong,

2
@rwong: è solo un problema nello schema ingenuo; puoi facilmente integrare un contatore di epoca, ad esempio, in modo tale che quando viene specificato un vecchio handle, otterrai una discrepanza di epoca.
Matthieu M.,

1
Suggerimento di @JonathonReinhart: puoi menzionare la "regola di aliasing rigorosa" nella tua domanda per aiutare a guidare la discussione verso gli aspetti più importanti.
rwong,

3

Ecco una situazione in cui è necessaria una maniglia opaca;

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

Quando la libreria ha due o più tipi di struttura che hanno la stessa parte di intestazione dei campi, come "tipo" in precedenza, questi tipi di struttura possono essere considerati aventi una struttura padre comune (come una classe base in C ++).

È possibile definire la parte di intestazione come "motore di struttura", in questo modo;

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

Ma è una decisione facoltativa perché i cast di tipi sono necessari indipendentemente dall'utilizzo del motore di struttura.

Conclusione

In alcuni casi, ci sono ragioni per cui vengono utilizzate maniglie opache invece di strutture con nome opaco.


Penso che l'uso di un sindacato lo renda più sicuro anziché pericoloso lanciare campi che potrebbero essere spostati. Dai un'occhiata a questo riassunto che ho messo insieme mostrando un esempio completo.
Jonathon Reinhart,

Ma in realtà, evitare il switchprimo, usando "funzioni virtuali" è probabilmente l'ideale e risolve l'intero problema.
Jonathon Reinhart,

Il tuo design in sostanza è più complesso di quanto suggerissi. Sicuramente, rende il casting meno, sicuro e intelligente, ma introduce più codice e tipi. A mio avviso, sembra essere troppo complicato per essere sicuro del tipo. Io e forse l'autore della biblioteca decido di seguire i KISS piuttosto che la sicurezza dei tipi.
Akio Takahashi,

Bene, se vuoi renderlo davvero semplice, potresti anche omettere completamente il controllo degli errori!
Jonathon Reinhart,

A mio avviso, la semplicità di progettazione è preferita rispetto a una serie di controlli degli errori. In questo caso, tali controlli degli errori esistono solo nelle funzioni API. Inoltre, puoi rimuovere i cast di tipo usando union, ma ricorda che l'unione è naturalmente non sicura.
Akio Takahashi,

2

Il vantaggio più evidente dell'approccio handle è che è possibile modificare le strutture interne senza interrompere l'API esterna. Certo, devi ancora modificare il software client, ma almeno non stai cambiando l'interfaccia.

L'altra cosa che fa è fornire la possibilità di scegliere tra diversi tipi possibili in fase di runtime, senza dover fornire un'interfaccia API esplicita per ognuno. Alcune applicazioni, come le letture dei sensori di diversi tipi di sensori in cui ciascun sensore è leggermente diverso e genera dati leggermente diversi, rispondono bene a questo approccio.

Dato che forniresti comunque le strutture ai tuoi clienti, sacrifichi un po 'di sicurezza del tipo (che può ancora essere verificato in fase di esecuzione) per un'API molto più semplice, anche se richiede un casting.


5
"Puoi modificare le strutture interne senza ..." - puoi anche con l'approccio di dichiarazione diretta.
user253751

L'approccio "dichiarazione anticipata" non richiede ancora di dichiarare le firme di tipo? E quelle firme di tipo non cambiano ancora se cambiate le strutture?
Robert Harvey,

La dichiarazione diretta richiede solo di dichiarare il nome del tipo: la struttura rimane nascosta.
Idan Arye,

Quindi quale sarebbe il vantaggio della dichiarazione anticipata se non imponesse nemmeno la struttura del tipo?
Robert Harvey,

6
@RobertHarvey Ricorda: stiamo parlando di C. Non ci sono metodi, quindi a parte il nome e la struttura non c'è nient'altro per il tipo. Se ha fatto rispettare la struttura sarebbe stato essere identico a dichiarazione regolare. Il punto di esporre il nome senza applicare la struttura è che è possibile utilizzare quel tipo nelle firme delle funzioni. Naturalmente, senza la struttura è possibile utilizzare solo puntatori al tipo, poiché il compilatore non è in grado di conoscerne le dimensioni, ma poiché non esiste un cast implicito di puntatori in C che utilizza i puntatori è abbastanza buono per la digitazione statica per proteggerti.
Idan Arye,

2

Già visto

In che modo una maniglia opaca è migliore di una struttura opaca con nome?

Ho incontrato il esatto stesso scenario, solo con alcune sottili differenze. Nel nostro SDK avevamo molte cose come questa:

typedef void* SomeHandle;

La mia mera proposta era di abbinarla ai nostri tipi interni:

typedef struct SomeVertex* SomeHandle;

Per le terze parti che utilizzano l'SDK, non dovrebbe fare alcuna differenza. È un tipo opaco. Che importa? Non ha alcun effetto sull'ABI * o sulla compatibilità dei sorgenti e l'utilizzo di nuove versioni dell'SDK richiede comunque la ricompilazione del plug-in.

* Nota che, come sottolinea gnasher, in realtà ci possono essere casi in cui la dimensione di qualcosa come puntatore a strutturare e annullare * potrebbe effettivamente avere una dimensione diversa nel qual caso influenzerebbe l'ABI. Come lui, non l'ho mai incontrato in pratica. Ma da quel punto di vista, il secondo potrebbe effettivamente migliorare la portabilità in un contesto oscuro, quindi questa è un'altra ragione per favorire il secondo, anche se probabilmente per la maggior parte delle persone.

Bug di terze parti

Inoltre, avevo ancora più cause della sicurezza dei tipi per lo sviluppo / debug interno. Avevamo già un certo numero di sviluppatori di plug-in che avevano dei bug nel loro codice perché due handle simili ( Panele PanelNew, ad esempio) utilizzavano entrambi un void*typedef per i loro handle, e stavano accidentalmente passando gli handle sbagliati nei posti sbagliati a causa del solo utilizzo void*per tutto. Quindi stava effettivamente causando bug sul lato di quelli che usavanol'SDK. I loro bug sono costati anche molto tempo al team di sviluppo interno, dal momento che avrebbero inviato segnalazioni di bug lamentando bug nel nostro SDK e avremmo dovuto eseguire il debug del plug-in e scoprire che era effettivamente causato da un bug nel plug-in che passava gli handle sbagliati nei posti sbagliati (che è facilmente consentito senza nemmeno un avviso quando ogni handle è un alias per void*o size_t). Quindi stavamo inutilmente sprecando il nostro tempo a fornire un servizio di debug per terze parti a causa di errori causati da parte nostra dal nostro desiderio di purezza concettuale nel nascondere tutte le informazioni interne, anche i semplici nomi dei nostri interni structs.

Mantenere il Typedef

La differenza è che io proponevo che facciamo bastone per il typedefclient di scrittura ancora, non avere struct SomeVertexche avrebbe influire sulla compatibilità fonte per le future versioni del plugin. Mentre personalmente mi piace l'idea di non scrivere structin C, dal punto di vista dell'SDK, typedefpuò essere d'aiuto poiché l'intero punto è l'opacità. Quindi lì suggerirei di rilassare questo standard solo per l'API esposta pubblicamente. Per i client che usano l'SDK, non dovrebbe importare se un handle è un puntatore a una struttura, un numero intero, ecc. L'unica cosa che conta per loro è che due handle diversi non aliasno lo stesso tipo di dati in modo che non lo facciano passare erroneamente nella maniglia sbagliata nel posto sbagliato.

Digitare informazioni

Dove è più importante evitare il casting è per te, gli sviluppatori interni. Questo tipo di estetica di nascondere tutti i nomi interni all'SDK è un'estetica concettuale che comporta un costo significativo per la perdita di tutte le informazioni di tipo e che richiede di spargere inutilmente i cast nei nostri debugger per ottenere informazioni critiche. Mentre un programmatore C dovrebbe essere ampiamente abituato a questo in C, richiedere questo inutilmente è solo chiedere guai.

Ideali concettuali

In generale, vuoi stare attento a quei tipi di sviluppatori che mettono un'idea concettuale di purezza molto al di sopra di tutte le esigenze pratiche e quotidiane. Questi porteranno a terra la manutenibilità della tua base di codice nel cercare un ideale utopico, facendo in modo che l'intera squadra eviti la lozione abbronzante in un deserto per paura che sia innaturale e possa causare una carenza di vitamina D mentre metà dell'equipaggio sta morendo di cancro alla pelle.

Preferenza fine utente

Anche dal punto di vista rigoroso da parte degli utenti di quelli che usano l'API, preferirebbero un'API buggy o un'API che funzioni bene ma espone un nome di cui non potrebbero preoccuparsene in cambio? Perché questo è il compromesso pratico. Perdere inutilmente informazioni di tipo al di fuori di un contesto generico aumenta il rischio di bug e da una base di codice su larga scala in un contesto a livello di team per un certo numero di anni, la legge di Murphy tende ad essere abbastanza applicabile. Se aumenti inutilmente il rischio di bug, è probabile che otterrai almeno altri bug. Non ci vuole molto tempo in una grande squadra per scoprire che ogni tipo di errore umano immaginabile alla fine passerà da un potenziale a una realtà.

Quindi forse questa è una domanda da porre agli utenti. "Preferiresti un SDK con buggier o uno che esponesse alcuni nomi opachi interni di cui non ti preoccuperai nemmeno?" E se questa domanda sembra presentare una falsa dicotomia, direi che è necessaria una maggiore esperienza a livello di squadra in un ambiente su larga scala per apprezzare il fatto che un rischio più elevato di bug alla fine manifesterà veri e propri bug a lungo termine. Poco importa quanto lo sviluppatore abbia fiducia nell'evitare i bug. In un ambiente a livello di squadra, aiuta di più a pensare ai collegamenti più deboli, e almeno ai modi più semplici e rapidi per impedire loro di inciampare.

Proposta

Quindi suggerirei un compromesso qui che ti darà comunque la possibilità di conservare tutti i vantaggi del debug:

typedef struct engine* enh;

... anche a costo di scrivere a macchina struct, questo ci ucciderà davvero? Probabilmente no, quindi raccomando anche un po 'di pragmatismo da parte tua, ma ancora di più allo sviluppatore che preferirebbe rendere il debug esponenzialmente più difficile utilizzando size_tqui e eseguendo il cast da / verso intero per nessuna buona ragione se non per nascondere ulteriormente informazioni che sono già 99 % nascosto all'utente e non può assolutamente fare più danni di size_t.


1
È una piccola differenza: secondo lo standard C, tutti i "puntatori a struct" hanno una rappresentazione identica, quindi tutti i "puntatori all'unione", quindi "void *" e "char *", ma un void * e un "pointer strutturare "può avere diverse dimensioni di () e / o rappresentazione diversa. In pratica, non l'ho mai visto.
gnasher729,

@ gnasher729 Lo stesso, forse dovrei qualificare quella parte rispetto alla potenziale perdita di portabilità in fusione di void*o size_te indietro come un altro motivo per evitare di proiettare superfluo. L'ho omesso dal momento che allo stesso modo non l'ho mai visto in pratica date le piattaforme che abbiamo preso di mira (che erano sempre piattaforme desktop: Linux, OSX, Windows).


1

Sospetto che il vero motivo sia l'inerzia, è quello che hanno sempre fatto e funziona, quindi perché cambiarlo?

Il motivo principale per cui riesco a vedere è che la maniglia opaca consente al designer di mettere qualsiasi cosa dietro, non solo una struttura. Se l'API ritorna e accetta più tipi opachi, tutti sembrano uguali al chiamante e non ci sono mai problemi di compilazione o ricompilazioni se la stampa fine cambia. Se en_NewFlidgetTwiddler (handle ** newTwiddler) cambia per restituire un puntatore a Twiddler anziché un handle, l'API non cambia e qualsiasi nuovo codice utilizzerà silenziosamente un puntatore prima che stesse utilizzando un handle. Inoltre, non c'è pericolo che il sistema operativo o qualsiasi altra cosa "aggiusti" silenziosamente il puntatore se viene oltrepassato i confini.

Lo svantaggio di ciò, ovviamente, è che il chiamante può alimentare qualsiasi cosa al suo interno. Hai una cosa a 64 bit? Spingerlo nello slot a 64 bit nella chiamata API e vedere cosa succede.

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

Entrambi compilano ma scommetto che solo uno di loro fa quello che vuoi.


1

Credo che l'atteggiamento derivi da una filosofia di lunga data per difendere un'API della biblioteca C dagli abusi dei principianti.

In particolare,

  • Gli autori della biblioteca sanno che è un puntatore alla struttura e i dettagli della struttura sono visibili al codice della biblioteca.
  • Tutti i programmatori esperti che usano la libreria sanno anche che è un puntatore ad alcune strutture opache;
    • Hanno avuto un'esperienza abbastanza dura e dolorosa da sapere di non pasticciare con i byte memorizzati in quelle strutture.
  • I programmatori inesperti non lo sanno.
    • Cercheranno di memcpydati opachi o incrementare i byte o parole all'interno della struct. Vai a fare hacking.

La contromisura tradizionale di lunga data è:

  • Masquerade il fatto che una maniglia opaca è in realtà un puntatore a una struttura opaca che esiste nello stesso spazio-memoria-processo.
    • Per farlo sostenendo che si tratta di un valore intero con lo stesso numero di bit di a void*
    • Per essere più cauto, mascherare anche i bit del puntatore, ad es
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

Questo è solo per dire che ha lunghe tradizioni; Non avevo un'opinione personale sul fatto che fosse giusto o sbagliato.


3
Fatta eccezione per il ridicolo xor, la mia soluzione offre anche quella sicurezza. Il cliente rimane ignaro delle dimensioni o dei contenuti della struttura, con l'ulteriore vantaggio della sicurezza del tipo. Non vedo come sia meglio abusare di size_t per contenere un puntatore.
Jonathon Reinhart,

@JonathonReinhart è estremamente improbabile che il cliente sia effettivamente ignaro della struttura. La domanda è di più: possono ottenere la struttura e possono restituire una versione modificata alla tua libreria. Non solo con l'open source, ma più in generale. La soluzione è il moderno partizionamento della memoria, non lo stupido XOR.
Móż,

Di cosa stai parlando? Tutto quello che sto dicendo è che non è possibile compilare alcun codice che tenti di dereferenziare un puntatore a detta struttura o fare qualsiasi cosa che richieda la conoscenza delle sue dimensioni. Certo, potresti memset (, 0,) sull'heap dell'intero processo, se davvero lo desideri.
Jonathon Reinhart,

6
Questa discussione sembra molto simile alla guardia contro Machiavelli . Se l'utente desidera passare la spazzatura alla mia API, non è possibile arrestarli. L'introduzione di un'interfaccia di tipo non sicuro come questa difficilmente aiuta in questo, poiché in realtà semplifica l'abuso accidentale dell'API.
ComicSansMS,

@ComicSansMS grazie per aver menzionato "accidentale", poiché è quello che sto davvero cercando di prevenire qui.
Jonathon Reinhart,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.