Mi piacciono tutte le risposte e mostrano in quanti modi puoi affrontare questo problema. Ma in particolare per una parte della tua domanda:
But I wouldn't want to have extension developers link against all of these right? If I provide another library specifically for the extension API, how would that interact with the core application components?
Bene, potresti richiedere agli sviluppatori di plug-in di collegarsi ad alcune librerie per far funzionare i loro plugin, se lo desideri, purché sia accettabile. Ho lavorato su alcune grandi applicazioni commerciali e abbiamo evitato questo in ogni caso. I nostri plugin non collegano (staticamente o dinamicamente) a nessuna delle librerie del nostro software. Il nostro software si collega in modo strettamente dinamico al plug-in.
Il modo per farlo è la spedizione dinamica. Se si dispone di un puntatore a funzione che punta a una funzione da qualche parte nello spazio esterno, allora il plugin non ha bisogno di collegarsi alla libreria che fornisce la sua implementazione per chiamare quella funzione. E questo è fondamentalmente il modo in cui le persone nel mio dominio lo hanno sempre fatto sia in C che in C ++. Potremmo definire un tipo di firma / ritorno del puntatore della funzione in questo modo:
// Returns a pointer to an interface in the system given an ID.
// Real version might define calling conventions and look a little
// more ugly.
typedef void* FetchInterface(int interface_id);
E un punto di ingresso plugin nei plug-in di terze parti scrive una funzione in questo modo:
// Imported and called by our host application.
DYLIB_EXPORT void plugin_entry_point(FetchInterface* fetch);
E potrebbe essere implementato in questo modo:
void plugin_entry_point(FetchInterface* fetch)
{
SomeInterface* some_interface = fetch(SomeInterface::id);
some_interface->do_something(...);
}
E la nostra applicazione ospite lo chiama e il plugin recupera le interfacce necessarie con le quali vuole lavorare tramite il puntatore di funzione che forniamo e invoca le funzioni su quelle interfacce. E SomeInterface
sopra è solo una tabella ( struct
) che contiene i propri puntatori di funzione.
Il plugin deve includere le intestazioni dal nostro SDK per funzionare, ovviamente, per vedere come vengono definite cose come struct SomeInterface
. Tuttavia, non è necessario collegarsi a nulla.
Ugh, puntatori vuoti e codifica simile a C in C ++!
Ci si potrebbe chiedere perché abbiamo questo codice tipo C non sicuro per la nostra API centrale invece di, ad esempio, interfacce astratte attraverso classi con funzioni virtuali pure (che eviterebbero comunque i requisiti di collegamento). Sfortunatamente quest'ultimo non è così compatibile tra i vari compilatori e non è ben compreso dagli altri FFI linguistici.
Favorendo C per la nostra API qui, abbiamo avuto terze parti che scrivono anche plugin per il nostro software in linguaggi come C # e Lua usando le loro interfacce di funzione estranee anche se non lo progettiamo per tale scopo, dal momento che gli FFI tendono a favore C (non necessariamente capiscono concetti in C ++ come sovraccarico di funzioni, funzioni virtuali, costruttori e distruttori). È simile al modo in cui trovi persone che usano OpenGL da così tante lingue adesso, anche se si trattava di un'API C non necessariamente utilizzata in tali lingue.
Quindi preferiamo questo tipo di aspetto di "compatibilità quasi universale" di un'API C "minimo comune denominatore". Quello che facciamo invece per rendere più facile lavorare con il nostro tipo di API C non sicura e di basso livello in altri linguaggi è fornire wrapper, come i wrapper C ++ che sono conformi a RAII e sicuri. E quando non lo facciamo per le lingue non abbiamo previsto che gli altri utilizzino i plugin per scrivere, come C #, le terze parti tendono a scrivere una wrapper lib sulla nostra API C per semplificare la scrittura di plugin in C #.
Compatibilità con versioni e versioni precedenti
Probabilmente uno dei requisiti più complessi è che alcuni dei prodotti su cui ho lavorato hanno requisiti di compatibilità con le versioni precedenti con plug-in obsoleti (avevamo plug-in scritti più di 15 anni fa che dovevano ancora funzionare con il software, incluso un adattatore per fare 32 i plug-bit funzionano ancora nelle versioni a 64 bit del nostro software).
Per questo l'approccio in stile C con struct
offre un po 'di respiro perché qualsiasi membro aggiunto alla fine di un struct
non influisce sul layout di memoria dei suoi campi precedenti. Pertanto, possiamo aggiungere nuovi campi del puntatore alla funzione nella parte inferiore di una tabella esistente di puntatori di funzione senza influire sulla compatibilità all'indietro con i plug-in precedenti che puntano a quelli structs
.
Ogni tanto c'è la tentazione di cambiare le interfacce esistenti con nuovi plugin in modi che vanno oltre l'aggiunta di nuove funzionalità in basso, e per questo dobbiamo fondamentalmente creare una nuova versione dell'interfaccia e fare un po 'di saltando sotto il cofano per fornire i plugin con l'interfaccia corretta. In realtà i nostri plugin passano le loro versioni lungo l'analogico FetchInterface
come in questo modo:
void plugin_entry_point(FetchInterface* fetch)
{
// Passing the PLUGIN_VERSION along here (defined in the SDK
// headers the plugin is using) can affect what interface is
// returned based on the plugin version.
SomeInterface* some_interface = fetch(PLUGIN_VERSION, SomeInterface::id);
some_interface->do_something(...);
}
Mantenere la retrocompatibilità con un così lungo retaggio può essere una vera PITA, non importa cosa, quindi vale la pena prestare molta attenzione a come si progettano le API per ridurre la probabilità di doverle deprecare, poiché un lungo requisito di compatibilità legacy rende "deprecato" più di un suggerimento di utilizzare nuove interfacce piuttosto che fornire agli sviluppatori la possibilità di rimuovere e interrompere il mantenimento del codice antico.