cannam@56
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
cannam@56
|
2
|
cannam@56
|
3 /*
|
cannam@56
|
4 Vamp
|
cannam@56
|
5
|
cannam@56
|
6 An API for audio analysis and feature extraction plugins.
|
cannam@56
|
7
|
cannam@56
|
8 Centre for Digital Music, Queen Mary, University of London.
|
cannam@56
|
9 Copyright 2006 Chris Cannam.
|
cannam@56
|
10
|
cannam@56
|
11 Permission is hereby granted, free of charge, to any person
|
cannam@56
|
12 obtaining a copy of this software and associated documentation
|
cannam@56
|
13 files (the "Software"), to deal in the Software without
|
cannam@56
|
14 restriction, including without limitation the rights to use, copy,
|
cannam@56
|
15 modify, merge, publish, distribute, sublicense, and/or sell copies
|
cannam@56
|
16 of the Software, and to permit persons to whom the Software is
|
cannam@56
|
17 furnished to do so, subject to the following conditions:
|
cannam@56
|
18
|
cannam@56
|
19 The above copyright notice and this permission notice shall be
|
cannam@56
|
20 included in all copies or substantial portions of the Software.
|
cannam@56
|
21
|
cannam@56
|
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
cannam@56
|
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
cannam@56
|
24 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
cannam@56
|
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
cannam@56
|
26 ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
cannam@56
|
27 CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
cannam@56
|
28 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
cannam@56
|
29
|
cannam@56
|
30 Except as contained in this notice, the names of the Centre for
|
cannam@56
|
31 Digital Music; Queen Mary, University of London; and Chris Cannam
|
cannam@56
|
32 shall not be used in advertising or otherwise to promote the sale,
|
cannam@56
|
33 use or other dealings in this Software without prior written
|
cannam@56
|
34 authorization.
|
cannam@56
|
35 */
|
cannam@56
|
36
|
cannam@59
|
37 #include "vamp-sdk/PluginHostAdapter.h"
|
cannam@56
|
38 #include "PluginLoader.h"
|
cannam@61
|
39 #include "PluginInputDomainAdapter.h"
|
cannam@61
|
40 #include "PluginChannelAdapter.h"
|
cannam@56
|
41
|
cannam@57
|
42 #include <fstream>
|
cannam@57
|
43
|
cannam@59
|
44 #ifdef _WIN32
|
cannam@59
|
45
|
cannam@59
|
46 #include <windows.h>
|
cannam@59
|
47 #include <tchar.h>
|
cannam@59
|
48 #define PLUGIN_SUFFIX "dll"
|
cannam@59
|
49
|
cannam@59
|
50 #else /* ! _WIN32 */
|
cannam@59
|
51
|
cannam@59
|
52 #include <dirent.h>
|
cannam@59
|
53 #include <dlfcn.h>
|
cannam@59
|
54
|
cannam@59
|
55 #ifdef __APPLE__
|
cannam@59
|
56 #define PLUGIN_SUFFIX "dylib"
|
cannam@59
|
57 #else /* ! __APPLE__ */
|
cannam@59
|
58 #define PLUGIN_SUFFIX "so"
|
cannam@59
|
59 #endif /* ! __APPLE__ */
|
cannam@59
|
60
|
cannam@59
|
61 #endif /* ! _WIN32 */
|
cannam@56
|
62
|
cannam@57
|
63 using namespace std;
|
cannam@57
|
64
|
cannam@56
|
65 namespace Vamp {
|
cannam@56
|
66
|
cannam@59
|
67 namespace HostExt {
|
cannam@59
|
68
|
cannam@59
|
69 PluginLoader *
|
cannam@59
|
70 PluginLoader::m_instance = 0;
|
cannam@59
|
71
|
cannam@56
|
72 PluginLoader::PluginLoader()
|
cannam@56
|
73 {
|
cannam@56
|
74 }
|
cannam@56
|
75
|
cannam@56
|
76 PluginLoader::~PluginLoader()
|
cannam@56
|
77 {
|
cannam@56
|
78 }
|
cannam@56
|
79
|
cannam@59
|
80 PluginLoader *
|
cannam@59
|
81 PluginLoader::getInstance()
|
cannam@59
|
82 {
|
cannam@59
|
83 if (!m_instance) m_instance = new PluginLoader();
|
cannam@59
|
84 return m_instance;
|
cannam@59
|
85 }
|
cannam@59
|
86
|
cannam@57
|
87 vector<PluginLoader::PluginKey>
|
cannam@56
|
88 PluginLoader::listPlugins()
|
cannam@56
|
89 {
|
cannam@59
|
90 if (m_pluginLibraryNameMap.empty()) generateLibraryMap();
|
cannam@56
|
91
|
cannam@57
|
92 vector<PluginKey> plugins;
|
cannam@57
|
93 for (map<PluginKey, string>::iterator mi =
|
cannam@59
|
94 m_pluginLibraryNameMap.begin();
|
cannam@59
|
95 mi != m_pluginLibraryNameMap.end(); ++mi) {
|
cannam@56
|
96 plugins.push_back(mi->first);
|
cannam@56
|
97 }
|
cannam@56
|
98
|
cannam@56
|
99 return plugins;
|
cannam@56
|
100 }
|
cannam@56
|
101
|
cannam@59
|
102 void
|
cannam@59
|
103 PluginLoader::generateLibraryMap()
|
cannam@59
|
104 {
|
cannam@59
|
105 vector<string> path = PluginHostAdapter::getPluginPath();
|
cannam@59
|
106
|
cannam@59
|
107 for (size_t i = 0; i < path.size(); ++i) {
|
cannam@59
|
108
|
cannam@59
|
109 vector<string> files = listFiles(path[i], PLUGIN_SUFFIX);
|
cannam@59
|
110
|
cannam@59
|
111 for (vector<string>::iterator fi = files.begin();
|
cannam@59
|
112 fi != files.end(); ++fi) {
|
cannam@59
|
113
|
cannam@59
|
114 string fullPath = path[i];
|
cannam@59
|
115 fullPath = splicePath(fullPath, *fi);
|
cannam@59
|
116 void *handle = loadLibrary(fullPath);
|
cannam@59
|
117 if (!handle) continue;
|
cannam@59
|
118
|
cannam@59
|
119 VampGetPluginDescriptorFunction fn =
|
cannam@59
|
120 (VampGetPluginDescriptorFunction)lookupInLibrary
|
cannam@59
|
121 (handle, "vampGetPluginDescriptor");
|
cannam@59
|
122
|
cannam@59
|
123 if (!fn) {
|
cannam@59
|
124 unloadLibrary(handle);
|
cannam@59
|
125 continue;
|
cannam@59
|
126 }
|
cannam@59
|
127
|
cannam@59
|
128 int index = 0;
|
cannam@59
|
129 const VampPluginDescriptor *descriptor = 0;
|
cannam@59
|
130
|
cannam@59
|
131 while ((descriptor = fn(VAMP_API_VERSION, index))) {
|
cannam@59
|
132 PluginKey key = composePluginKey(*fi, descriptor->identifier);
|
cannam@59
|
133 if (m_pluginLibraryNameMap.find(key) ==
|
cannam@59
|
134 m_pluginLibraryNameMap.end()) {
|
cannam@59
|
135 m_pluginLibraryNameMap[key] = fullPath;
|
cannam@59
|
136 }
|
cannam@59
|
137 ++index;
|
cannam@59
|
138 }
|
cannam@59
|
139
|
cannam@59
|
140 unloadLibrary(handle);
|
cannam@59
|
141 }
|
cannam@59
|
142 }
|
cannam@59
|
143 }
|
cannam@59
|
144
|
cannam@59
|
145 PluginLoader::PluginKey
|
cannam@59
|
146 PluginLoader::composePluginKey(string libraryName, string identifier)
|
cannam@59
|
147 {
|
cannam@59
|
148 string basename = libraryName;
|
cannam@59
|
149
|
cannam@59
|
150 string::size_type li = basename.rfind('/');
|
cannam@59
|
151 if (li != string::npos) basename = basename.substr(li + 1);
|
cannam@59
|
152
|
cannam@59
|
153 li = basename.find('.');
|
cannam@59
|
154 if (li != string::npos) basename = basename.substr(0, li);
|
cannam@59
|
155
|
cannam@59
|
156 return basename + ":" + identifier;
|
cannam@59
|
157 }
|
cannam@59
|
158
|
cannam@57
|
159 PluginLoader::PluginCategoryHierarchy
|
cannam@57
|
160 PluginLoader::getPluginCategory(PluginKey plugin)
|
cannam@57
|
161 {
|
cannam@57
|
162 if (m_taxonomy.empty()) generateTaxonomy();
|
cannam@57
|
163 if (m_taxonomy.find(plugin) == m_taxonomy.end()) return PluginCategoryHierarchy();
|
cannam@57
|
164 return m_taxonomy[plugin];
|
cannam@57
|
165 }
|
cannam@57
|
166
|
cannam@57
|
167 string
|
cannam@57
|
168 PluginLoader::getLibraryPathForPlugin(PluginKey plugin)
|
cannam@56
|
169 {
|
cannam@59
|
170 if (m_pluginLibraryNameMap.empty()) generateLibraryMap();
|
cannam@59
|
171 if (m_pluginLibraryNameMap.find(plugin) == m_pluginLibraryNameMap.end()) return "";
|
cannam@59
|
172 return m_pluginLibraryNameMap[plugin];
|
cannam@56
|
173 }
|
cannam@56
|
174
|
cannam@56
|
175 Plugin *
|
cannam@61
|
176 PluginLoader::loadPlugin(PluginKey key, float inputSampleRate, int adapterFlags)
|
cannam@56
|
177 {
|
cannam@57
|
178 string fullPath = getLibraryPathForPlugin(key);
|
cannam@56
|
179 if (fullPath == "") return 0;
|
cannam@56
|
180
|
cannam@57
|
181 string::size_type ki = key.find(':');
|
cannam@57
|
182 if (ki == string::npos) {
|
cannam@56
|
183 //!!! flag error
|
cannam@56
|
184 return 0;
|
cannam@56
|
185 }
|
cannam@56
|
186
|
cannam@57
|
187 string identifier = key.substr(ki + 1);
|
cannam@56
|
188
|
cannam@59
|
189 void *handle = loadLibrary(fullPath);
|
cannam@59
|
190 if (!handle) return 0;
|
cannam@56
|
191
|
cannam@56
|
192 VampGetPluginDescriptorFunction fn =
|
cannam@59
|
193 (VampGetPluginDescriptorFunction)lookupInLibrary
|
cannam@56
|
194 (handle, "vampGetPluginDescriptor");
|
cannam@56
|
195
|
cannam@56
|
196 if (!fn) {
|
cannam@59
|
197 unloadLibrary(handle);
|
cannam@56
|
198 return 0;
|
cannam@56
|
199 }
|
cannam@56
|
200
|
cannam@56
|
201 int index = 0;
|
cannam@56
|
202 const VampPluginDescriptor *descriptor = 0;
|
cannam@56
|
203
|
cannam@56
|
204 while ((descriptor = fn(VAMP_API_VERSION, index))) {
|
cannam@59
|
205
|
cannam@57
|
206 if (string(descriptor->identifier) == identifier) {
|
cannam@59
|
207
|
cannam@59
|
208 Vamp::PluginHostAdapter *plugin =
|
cannam@59
|
209 new Vamp::PluginHostAdapter(descriptor, inputSampleRate);
|
cannam@59
|
210
|
cannam@61
|
211 Plugin *adapter = new PluginDeletionNotifyAdapter(plugin, this);
|
cannam@59
|
212
|
cannam@59
|
213 m_pluginLibraryHandleMap[adapter] = handle;
|
cannam@61
|
214
|
cannam@61
|
215 if (adapterFlags & ADAPT_INPUT_DOMAIN) {
|
cannam@61
|
216 if (adapter->getInputDomain() == Plugin::FrequencyDomain) {
|
cannam@61
|
217 adapter = new PluginInputDomainAdapter(adapter);
|
cannam@61
|
218 }
|
cannam@61
|
219 }
|
cannam@61
|
220
|
cannam@61
|
221 if (adapterFlags & ADAPT_CHANNEL_COUNT) {
|
cannam@61
|
222 adapter = new PluginChannelAdapter(adapter);
|
cannam@61
|
223 }
|
cannam@61
|
224
|
cannam@59
|
225 return adapter;
|
cannam@56
|
226 }
|
cannam@59
|
227
|
cannam@56
|
228 ++index;
|
cannam@56
|
229 }
|
cannam@59
|
230
|
cannam@59
|
231 cerr << "Vamp::HostExt::PluginLoader: Plugin \""
|
cannam@59
|
232 << identifier << "\" not found in library \""
|
cannam@59
|
233 << fullPath << "\"" << endl;
|
cannam@59
|
234
|
cannam@56
|
235 return 0;
|
cannam@56
|
236 }
|
cannam@56
|
237
|
cannam@57
|
238 void
|
cannam@57
|
239 PluginLoader::generateTaxonomy()
|
cannam@57
|
240 {
|
cannam@57
|
241 // cerr << "PluginLoader::generateTaxonomy" << endl;
|
cannam@57
|
242
|
cannam@57
|
243 vector<string> path = PluginHostAdapter::getPluginPath();
|
cannam@57
|
244 string libfragment = "/lib/";
|
cannam@57
|
245 vector<string> catpath;
|
cannam@57
|
246
|
cannam@57
|
247 string suffix = "cat";
|
cannam@57
|
248
|
cannam@57
|
249 for (vector<string>::iterator i = path.begin();
|
cannam@57
|
250 i != path.end(); ++i) {
|
cannam@59
|
251
|
cannam@59
|
252 // It doesn't matter that we're using literal forward-slash in
|
cannam@59
|
253 // this bit, as it's only relevant if the path contains
|
cannam@59
|
254 // "/lib/", which is only meaningful and only plausible on
|
cannam@59
|
255 // systems with forward-slash delimiters
|
cannam@57
|
256
|
cannam@57
|
257 string dir = *i;
|
cannam@57
|
258 string::size_type li = dir.find(libfragment);
|
cannam@57
|
259
|
cannam@57
|
260 if (li != string::npos) {
|
cannam@57
|
261 catpath.push_back
|
cannam@57
|
262 (dir.substr(0, li)
|
cannam@57
|
263 + "/share/"
|
cannam@57
|
264 + dir.substr(li + libfragment.length()));
|
cannam@57
|
265 }
|
cannam@57
|
266
|
cannam@57
|
267 catpath.push_back(dir);
|
cannam@57
|
268 }
|
cannam@57
|
269
|
cannam@57
|
270 char buffer[1024];
|
cannam@57
|
271
|
cannam@57
|
272 for (vector<string>::iterator i = catpath.begin();
|
cannam@57
|
273 i != catpath.end(); ++i) {
|
cannam@57
|
274
|
cannam@59
|
275 vector<string> files = listFiles(*i, suffix);
|
cannam@57
|
276
|
cannam@57
|
277 for (vector<string>::iterator fi = files.begin();
|
cannam@57
|
278 fi != files.end(); ++fi) {
|
cannam@57
|
279
|
cannam@59
|
280 string filepath = splicePath(*i, *fi);
|
cannam@57
|
281 ifstream is(filepath.c_str(), ifstream::in | ifstream::binary);
|
cannam@57
|
282
|
cannam@57
|
283 if (is.fail()) {
|
cannam@57
|
284 // cerr << "failed to open: " << filepath << endl;
|
cannam@57
|
285 continue;
|
cannam@57
|
286 }
|
cannam@57
|
287
|
cannam@57
|
288 // cerr << "opened: " << filepath << endl;
|
cannam@57
|
289
|
cannam@57
|
290 while (!!is.getline(buffer, 1024)) {
|
cannam@57
|
291
|
cannam@57
|
292 string line(buffer);
|
cannam@57
|
293
|
cannam@57
|
294 // cerr << "line = " << line << endl;
|
cannam@57
|
295
|
cannam@57
|
296 string::size_type di = line.find("::");
|
cannam@57
|
297 if (di == string::npos) continue;
|
cannam@57
|
298
|
cannam@57
|
299 string id = line.substr(0, di);
|
cannam@57
|
300 string encodedCat = line.substr(di + 2);
|
cannam@57
|
301
|
cannam@57
|
302 if (id.substr(0, 5) != "vamp:") continue;
|
cannam@57
|
303 id = id.substr(5);
|
cannam@57
|
304
|
cannam@57
|
305 while (encodedCat.length() >= 1 &&
|
cannam@57
|
306 encodedCat[encodedCat.length()-1] == '\r') {
|
cannam@57
|
307 encodedCat = encodedCat.substr(0, encodedCat.length()-1);
|
cannam@57
|
308 }
|
cannam@57
|
309
|
cannam@57
|
310 // cerr << "id = " << id << ", cat = " << encodedCat << endl;
|
cannam@57
|
311
|
cannam@57
|
312 PluginCategoryHierarchy category;
|
cannam@57
|
313 string::size_type ai;
|
cannam@57
|
314 while ((ai = encodedCat.find(" > ")) != string::npos) {
|
cannam@57
|
315 category.push_back(encodedCat.substr(0, ai));
|
cannam@57
|
316 encodedCat = encodedCat.substr(ai + 3);
|
cannam@57
|
317 }
|
cannam@57
|
318 if (encodedCat != "") category.push_back(encodedCat);
|
cannam@57
|
319
|
cannam@57
|
320 m_taxonomy[id] = category;
|
cannam@57
|
321 }
|
cannam@57
|
322 }
|
cannam@57
|
323 }
|
cannam@57
|
324 }
|
cannam@57
|
325
|
cannam@59
|
326 void *
|
cannam@59
|
327 PluginLoader::loadLibrary(string path)
|
cannam@59
|
328 {
|
cannam@59
|
329 void *handle = 0;
|
cannam@59
|
330 #ifdef _WIN32
|
cannam@59
|
331 handle = LoadLibrary(path.c_str());
|
cannam@59
|
332 if (!handle) {
|
cannam@59
|
333 cerr << "Vamp::HostExt::PluginLoader: Unable to load library \""
|
cannam@59
|
334 << path << "\"" << endl;
|
cannam@59
|
335 }
|
cannam@59
|
336 #else
|
cannam@59
|
337 handle = dlopen(path.c_str(), RTLD_LAZY);
|
cannam@59
|
338 if (!handle) {
|
cannam@59
|
339 cerr << "Vamp::HostExt::PluginLoader: Unable to load library \""
|
cannam@59
|
340 << path << "\": " << dlerror() << endl;
|
cannam@59
|
341 }
|
cannam@59
|
342 #endif
|
cannam@59
|
343 return handle;
|
cannam@59
|
344 }
|
cannam@59
|
345
|
cannam@59
|
346 void
|
cannam@59
|
347 PluginLoader::unloadLibrary(void *handle)
|
cannam@59
|
348 {
|
cannam@59
|
349 #ifdef _WIN32
|
cannam@59
|
350 FreeLibrary((HINSTANCE)handle);
|
cannam@59
|
351 #else
|
cannam@59
|
352 dlclose(handle);
|
cannam@59
|
353 #endif
|
cannam@59
|
354 }
|
cannam@59
|
355
|
cannam@59
|
356 void *
|
cannam@59
|
357 PluginLoader::lookupInLibrary(void *handle, const char *symbol)
|
cannam@59
|
358 {
|
cannam@59
|
359 #ifdef _WIN32
|
cannam@59
|
360 return (void *)GetProcAddress((HINSTANCE)handle, symbol);
|
cannam@59
|
361 #else
|
cannam@59
|
362 return (void *)dlsym(handle, symbol);
|
cannam@59
|
363 #endif
|
cannam@59
|
364 }
|
cannam@59
|
365
|
cannam@59
|
366 string
|
cannam@59
|
367 PluginLoader::splicePath(string a, string b)
|
cannam@59
|
368 {
|
cannam@59
|
369 #ifdef _WIN32
|
cannam@59
|
370 return a + "\\" + b;
|
cannam@59
|
371 #else
|
cannam@59
|
372 return a + "/" + b;
|
cannam@59
|
373 #endif
|
cannam@59
|
374 }
|
cannam@59
|
375
|
cannam@59
|
376 vector<string>
|
cannam@59
|
377 PluginLoader::listFiles(string dir, string extension)
|
cannam@59
|
378 {
|
cannam@59
|
379 vector<string> files;
|
cannam@59
|
380 size_t extlen = extension.length();
|
cannam@59
|
381
|
cannam@59
|
382 #ifdef _WIN32
|
cannam@59
|
383
|
cannam@59
|
384 string expression = dir + "\\*." + extension;
|
cannam@59
|
385 WIN32_FIND_DATA data;
|
cannam@59
|
386 HANDLE fh = FindFirstFile(expression.c_str(), &data);
|
cannam@59
|
387 if (fh == INVALID_HANDLE_VALUE) return files;
|
cannam@59
|
388
|
cannam@59
|
389 bool ok = true;
|
cannam@59
|
390 while (ok) {
|
cannam@59
|
391 files.push_back(data.cFileName);
|
cannam@59
|
392 ok = FindNextFile(fh, &data);
|
cannam@59
|
393 }
|
cannam@59
|
394
|
cannam@59
|
395 FindClose(fh);
|
cannam@59
|
396
|
cannam@59
|
397 #else
|
cannam@59
|
398 DIR *d = opendir(dir.c_str());
|
cannam@59
|
399 if (!d) return files;
|
cannam@59
|
400
|
cannam@59
|
401 struct dirent *e = 0;
|
cannam@59
|
402 while ((e = readdir(d))) {
|
cannam@59
|
403
|
cannam@59
|
404 if (!(e->d_type & DT_REG) || !e->d_name) continue;
|
cannam@59
|
405
|
cannam@59
|
406 size_t len = strlen(e->d_name);
|
cannam@59
|
407 if (len < extlen + 2 ||
|
cannam@59
|
408 e->d_name + len - extlen - 1 != "." + extension) {
|
cannam@59
|
409 continue;
|
cannam@59
|
410 }
|
cannam@59
|
411
|
cannam@59
|
412 files.push_back(e->d_name);
|
cannam@59
|
413 }
|
cannam@59
|
414
|
cannam@59
|
415 closedir(d);
|
cannam@59
|
416 #endif
|
cannam@59
|
417
|
cannam@59
|
418 return files;
|
cannam@59
|
419 }
|
cannam@59
|
420
|
cannam@59
|
421 void
|
cannam@59
|
422 PluginLoader::pluginDeleted(PluginDeletionNotifyAdapter *adapter)
|
cannam@59
|
423 {
|
cannam@59
|
424 void *handle = m_pluginLibraryHandleMap[adapter];
|
cannam@59
|
425 if (handle) unloadLibrary(handle);
|
cannam@59
|
426 m_pluginLibraryHandleMap.erase(adapter);
|
cannam@59
|
427 }
|
cannam@59
|
428
|
cannam@59
|
429 PluginLoader::PluginDeletionNotifyAdapter::PluginDeletionNotifyAdapter(Plugin *plugin,
|
cannam@59
|
430 PluginLoader *loader) :
|
cannam@59
|
431 PluginWrapper(plugin),
|
cannam@59
|
432 m_loader(loader)
|
cannam@59
|
433 {
|
cannam@59
|
434 }
|
cannam@59
|
435
|
cannam@59
|
436 PluginLoader::PluginDeletionNotifyAdapter::~PluginDeletionNotifyAdapter()
|
cannam@59
|
437 {
|
cannam@59
|
438 // We need to delete the plugin before calling pluginDeleted, as
|
cannam@59
|
439 // the delete call may require calling through to the descriptor
|
cannam@59
|
440 // (for e.g. cleanup) but pluginDeleted may unload the required
|
cannam@59
|
441 // library for the call. To prevent a double deletion when our
|
cannam@59
|
442 // parent's destructor runs (after this one), be sure to set
|
cannam@59
|
443 // m_plugin to 0 after deletion.
|
cannam@59
|
444 delete m_plugin;
|
cannam@59
|
445 m_plugin = 0;
|
cannam@59
|
446
|
cannam@59
|
447 if (m_loader) m_loader->pluginDeleted(this);
|
cannam@59
|
448 }
|
cannam@57
|
449
|
cannam@57
|
450 }
|
cannam@59
|
451
|
cannam@59
|
452 }
|