Progettazione per l'estensibilità di terze parti

5

Ho difficoltà a capire come fornire un'API a terze parti per consentire le estensioni per le applicazioni desktop. Capisco che se sto usando un linguaggio compilato (ad esempio C ++), posso caricare le librerie dinamiche come estensioni in fase di esecuzione, purché si colleghino alla mia libreria e forniscano un'API ben definita a cui il codice di base richiama. La parte con cui sto combattendo è il modo in cui il componente API si inserisce nel resto della mia architettura. In genere avrei la mia applicazione divisa in più librerie (lib, modelli, ui, librerie specifiche per le funzioni, ecc.). Ma non vorrei che gli sviluppatori di estensioni si colleghino a tutti questi diritti? Se fornisco un'altra libreria specifica per l'API di estensione, in che modo interagire con i componenti principali dell'applicazione? In che modo le ben note applicazioni estendibili (IDE, app di editing foto / audio / video) implementano questo?

    
posta M. Bise 11.07.2018 - 03:32
fonte

5 risposte

6

Lavoro su un'API per un'applicazione di editing video. Abbiamo SDK separati (e quindi API) per diversi tipi di estensioni. C'è un set di API per l'elaborazione di immagini / video. C'è un altro set di API per l'importazione di video di diversi formati nell'applicazione. C'è un altro set di API per esplorare il modello di documento. Ogni API ha un kit di sviluppo software (SDK) che contiene una libreria e intestazioni che gli sviluppatori possono creare per creare un'estensione. (E comprendono la documentazione e il codice di esempio per mostrare come funzionano.)

Architettonicamente, la tua API di estensione varia in base al tipo di lavoro che è necessario raggiungere. Per l'elaborazione delle immagini, ci sono alcune parti principali:

  1. Capire quali sono le funzionalità del plug-in (può eseguire il rendering utilizzando le trame GPU o ha bisogno di bitmap sulla CPU?) In quale spazio colore funziona? ecc.
  2. fornire elementi dell'interfaccia utente come un nome per la tua estensione, controlli che l'utente può impostare, ecc.
  3. per dare immagini di input all'estensione e recuperare le immagini di output

Internamente, quando l'applicazione deve elaborare un frame di video, chiama un'estensione per leggere il frame dal disco. Una volta ottenuto il frame, esegue alcune impostazioni con l'estensione per l'elaborazione delle immagini. Quindi chiama l'estensione per il rendering e l'estensione richiama l'applicazione per ottenere informazioni come i valori dei vari cursori inseriti nell'interfaccia utente. Esegue la sua elaborazione e inserisce il risultato nel frame di output. L'app quindi dice all'estensione di fare qualsiasi teardown per-rendering che deve fare.

Se si guardano le API popolari come l'API di Photoshop, forniscono una sorta di struttura dati o oggetto che un'estensione può chiamare per ottenere informazioni dall'applicazione host o fornire informazioni ad essa. L'estensione stessa deve implementare un certo insieme di funzioni o metodi che l'applicazione chiamerà. Esiste generalmente un flusso di dati ben definito tra l'applicazione host e un'estensione, ma i dettagli variano a seconda del tipo di plug-in e persino dell'applicazione specifica che implementa l'API.

    
risposta data 11.07.2018 - 07:39
fonte
2

Un'API plugin è molto simile a una classe base (completamente) astratta. Potrebbe anche essere espresso come una classe base astratta. Sono richieste le funzioni di esportazione del plugin di un determinato nome e firma. L'applicazione chiama quindi queste funzioni

extern void Frobnicate(Frobber frob);
extern Bazzer MakeBazzer();

O si definisce una classe base e si richiede che il plugin esporti una funzione che restituisca un'istanza di quella classe.

class FooPlugin 
{
public:
    virtual ~FooPlugin() = default();
    void Frobnicate(Frobber frob) = 0;
    Bazzer MakeBazzer() = 0;
}

extern FooPlugin & PluginInitialise();

Ha solo bisogno di definizioni dei tipi utilizzati nell'interfaccia pubblica e può essere altrimenti isolato dalle definizioni nel resto del programma.

    
risposta data 11.07.2018 - 10:59
fonte
1

L'approccio particolare che utilizzi per gestire l'integrazione dipenderà dal tuo sistema linguistico e dalle esigenze di prestazioni. Non esiste una risposta valida per tutti.

If you are using C++, and have a speed critical application (e.g. passing large amounts of data in and out of the plugin), you might use a .so file (dll in windows), and have a simple abstract interface for your plugin API. This is technically challenging, as there are 'marshaling' and 'linkage' issues you have to be careful about.

Another approach that works quite well is json-messaging. You can have your plugin operate as a web-service, where you send messages (and get answers). Or a slightly simpler variant of that pattern - is pipes: you have your plugin take a json-payload argument (in stdin) and return a json-formatted result through stdout). This is not super efficient (but with care can be pretty efficient). But it has the benefit of being highly decoupled - with the plugin being implemented in any language/system, and the calling code using any other language/platform/system. {obviously there is nothing special about using json in the above, just a common way to do this, and if you asked me 10 years ago, I would have said the same thing but using xml}.

    
risposta data 11.07.2018 - 22:11
fonte
1

Un modo per rendere disponibili le funzionalità dell'applicazione principale al plug-in può essere studiato quando si utilizzano applicazioni come MS Word, MS Excel o Autocad, quando si utilizzano i loro meccanismi di plugin:

  • forniscono infatti un'API programmatica completa alla maggior parte delle funzionalità del programma. (Per gli esempi forniti, è in realtà la stessa API disponibile per i programmi VBA).

  • la "libreria specifica per l'API di estensione" che hai citato utilizza una tecnologia che supporta l'associazione dinamica tardiva (nel caso di Word, Excel o Autocad è la tecnologia COM di Microsoft).

Ciò consentirà di collegare il plug-in solo contro una libreria stub senza dover eseguire il collegamento con l'intera applicazione principale. La stub lib utilizza quindi il collegamento dinamico per comunicare con il programma principale.

Naturalmente, in C ++ un insieme di classi base astratte che forniscono un'interfaccia astratta alle parti dell'applicazione principale che si desidera esporre ai plugin potrebbe essere sufficiente (nel caso in cui si preveda che gli sviluppatori di plugin utilizzino lo stesso compilatore e la versione di lib di runtime come è usato per l'applicazione principale).

    
risposta data 11.07.2018 - 21:43
fonte
0

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.

    
risposta data 13.12.2018 - 12:39
fonte

Leggi altre domande sui tag