Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2026 by Thun Lu. All rights reserved.
3 : : * Author: Thun Lu <thun.lu@zohomail.cn>
4 : : * Repo: https://github.com/thun-res/vlink
5 : : * _ __ __ _ __
6 : : * | | / / / / (_) ____ / /__
7 : : * | | / / / / / / / __ \ / //_/
8 : : * | |/ / / /___ / / / / / / / ,<
9 : : * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10 : : *
11 : : * Licensed under the Apache License, Version 2.0 (the "License");
12 : : * you may not use this file except in compliance with the License.
13 : : * You may obtain a copy of the License at
14 : : *
15 : : * http://www.apache.org/licenses/LICENSE-2.0
16 : : *
17 : : * Unless required by applicable law or agreed to in writing, software
18 : : * distributed under the License is distributed on an "AS IS" BASIS,
19 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 : : * See the License for the specific language governing permissions and
21 : : * limitations under the License.
22 : : */
23 : :
24 : : #include "./zerocopy/fast_buffer_manager.h"
25 : :
26 : : #include <array>
27 : : #include <atomic>
28 : : #include <cstdlib>
29 : : #include <cstring>
30 : : #include <filesystem>
31 : : #include <functional>
32 : : #include <limits>
33 : : #include <memory>
34 : : #include <mutex>
35 : : #include <new>
36 : : #include <string>
37 : : #include <string_view>
38 : : #include <unordered_map>
39 : : #include <utility>
40 : :
41 : : #include "./base/memory_pool.h"
42 : : #include "./base/sys_sharemem.h"
43 : : #include "./base/utils.h"
44 : : #include "./base/uuid.h"
45 : :
46 : : #if defined(__linux__)
47 : : #include <unistd.h>
48 : : #endif
49 : :
50 : : namespace vlink {
51 : :
52 : : static constexpr size_t kFastBufferNameSize = 32;
53 : : static constexpr size_t kFastBufferMaxReaders = 32;
54 : : static constexpr std::string_view kFastBufferControlPrefix = "/vfb_";
55 : : static constexpr std::string_view kFastBufferMemoryPrefix = "/vfbm_";
56 : :
57 : : template <typename T, typename... ArgsT>
58 : 90 : static T* pool_new(ArgsT&&... args) noexcept {
59 : 90 : void* memory = MemoryPool::global_instance().allocate(sizeof(T), alignof(T));
60 [ + - ]: 180 : return memory ? new (memory) T(std::forward<ArgsT>(args)...) : nullptr;
61 : : }
62 : :
63 : : template <typename T>
64 : 120 : static void pool_delete(T* object) noexcept {
65 [ + + ]: 120 : if (object) {
66 : 90 : object->~T();
67 : 90 : MemoryPool::global_instance().deallocate(object, sizeof(T), alignof(T));
68 : : }
69 : 120 : }
70 : :
71 : : // PoolDeleter
72 : : struct PoolDeleter final {
73 : : template <typename T>
74 : 12 : void operator()(T* object) const noexcept {
75 : 12 : pool_delete(object);
76 : 12 : }
77 : : };
78 : :
79 : : template <typename T>
80 : : using PoolPtr = std::unique_ptr<T, PoolDeleter>;
81 : :
82 : : // PoolAllocator
83 : : template <typename T>
84 : : struct PoolAllocator final {
85 : : using value_type = T;
86 : :
87 : : PoolAllocator() noexcept = default;
88 : :
89 : : template <typename U>
90 : 8 : PoolAllocator(const PoolAllocator<U>&) noexcept {} // NOLINT(google-explicit-constructor)
91 : :
92 : 16 : [[nodiscard]] T* allocate(size_t count) {
93 : 16 : void* memory = MemoryPool::global_instance().allocate(count * sizeof(T), alignof(T));
94 : :
95 [ - + ]: 16 : if VUNLIKELY (!memory) {
96 : 0 : throw std::bad_alloc();
97 : : }
98 : :
99 : 16 : return static_cast<T*>(memory);
100 : : }
101 : :
102 : 16 : void deallocate(T* memory, size_t count) noexcept {
103 : 16 : MemoryPool::global_instance().deallocate(static_cast<void*>(memory), count * sizeof(T), alignof(T));
104 : 16 : }
105 : :
106 : : template <typename U>
107 : : bool operator==(const PoolAllocator<U>&) const noexcept {
108 : : return true;
109 : : }
110 : :
111 : : template <typename U>
112 : : bool operator!=(const PoolAllocator<U>&) const noexcept {
113 : : return false;
114 : : }
115 : : };
116 : :
117 : 15 : static uint64_t generate_id() noexcept {
118 : 15 : const Uuid uuid = Uuid::generate_random();
119 : 15 : uint64_t id = 0;
120 : 15 : std::memcpy(&id, uuid.bytes().data(), sizeof(id));
121 : 15 : return id;
122 : : }
123 : :
124 : 26 : static void format_name(std::string_view prefix, uint64_t id, char (&name)[kFastBufferNameSize]) noexcept {
125 : : static constexpr char kHexDigits[] = "0123456789abcdef";
126 : 26 : char* cursor = name;
127 : :
128 : 26 : std::memcpy(cursor, prefix.data(), prefix.size());
129 : 26 : cursor += prefix.size();
130 : :
131 [ + + ]: 442 : for (int shift = 60; shift >= 0; shift -= 4) {
132 : 416 : *cursor++ = kHexDigits[(id >> shift) & 0x0F];
133 : : }
134 : :
135 : 26 : *cursor = '\0';
136 : 26 : }
137 : :
138 : 61 : static bool parse_name(const char* name, std::string_view prefix, uint64_t& id) noexcept {
139 [ + - + + : 89 : if (std::memchr(name, '\0', kFastBufferNameSize) == nullptr || std::strlen(name) != prefix.size() + 16 ||
+ + ]
140 [ - + ]: 28 : std::strncmp(name, prefix.data(), prefix.size()) != 0) {
141 : 33 : return false;
142 : : }
143 : :
144 : 28 : uint64_t parsed = 0;
145 : :
146 [ + + ]: 476 : for (const char* cursor = name + prefix.size(); *cursor != '\0'; ++cursor) {
147 : 448 : const char digit = *cursor;
148 : 448 : uint64_t value = 0;
149 : :
150 [ + - + + ]: 448 : if (digit >= '0' && digit <= '9') {
151 : 290 : value = static_cast<uint64_t>(digit - '0');
152 [ + - + - ]: 158 : } else if (digit >= 'a' && digit <= 'f') {
153 : 158 : value = static_cast<uint64_t>(digit - 'a') + 10;
154 : : } else {
155 : 0 : return false;
156 : : }
157 : :
158 : 448 : parsed = (parsed << 4) | value;
159 : : }
160 : :
161 : 28 : id = parsed;
162 : 28 : return true;
163 : : }
164 : :
165 : 28 : static uint32_t fold_start_time(uint64_t start_time) noexcept {
166 : 28 : return static_cast<uint32_t>(start_time ^ (start_time >> 32));
167 : : }
168 : :
169 : 9 : static uint64_t process_identity(int32_t pid) noexcept {
170 : 9 : const uint64_t start_time = Utils::get_process_start_time(pid);
171 : :
172 [ + - - + ]: 9 : if (start_time == 0 || start_time == Utils::kUnknownProcessStartTime) {
173 : 0 : return 0;
174 : : }
175 : :
176 : 9 : return (static_cast<uint64_t>(static_cast<uint32_t>(pid)) << 32) | fold_start_time(start_time);
177 : : }
178 : :
179 : 18 : static uint64_t namespace_inode(const char* path) noexcept {
180 : : #if defined(__linux__)
181 : : char link[64];
182 : 18 : const ssize_t length = ::readlink(path, link, sizeof(link) - 1);
183 : :
184 [ - + ]: 18 : if (length <= 0) {
185 : 0 : return 0;
186 : : }
187 : :
188 : 18 : link[length] = '\0';
189 : 18 : const char* open = std::strchr(link, '[');
190 [ + - ]: 18 : return open ? std::strtoull(open + 1, nullptr, 10) : 0;
191 : : #else
192 : : (void)path;
193 : : return 0;
194 : : #endif
195 : : }
196 : :
197 : 23 : static bool is_identity_alive(uint64_t identity) noexcept {
198 : 23 : const uint64_t start_time = Utils::get_process_start_time(static_cast<int32_t>(identity >> 32));
199 : :
200 [ + + ]: 23 : if (start_time == 0) {
201 : 4 : return false;
202 : : }
203 : :
204 [ + - ]: 38 : return start_time == Utils::kUnknownProcessStartTime ||
205 [ + - ]: 38 : fold_start_time(start_time) == static_cast<uint32_t>(identity);
206 : : }
207 : :
208 : : template <typename FnT>
209 : 14 : static void sweep_segments(std::string_view prefix, FnT&& fn) noexcept {
210 : : #if defined(__linux__) || defined(__QNX__)
211 : : #if defined(__QNX__)
212 : : static constexpr const char* kDirectory = "/dev/shmem";
213 : : #else
214 : : static constexpr const char* kDirectory = "/dev/shm";
215 : : #endif
216 : 14 : std::error_code ec;
217 : 14 : std::filesystem::directory_iterator iter(kDirectory, ec);
218 : :
219 [ + + + - : 148 : for (; !ec && iter != std::filesystem::directory_iterator(); iter.increment(ec)) {
+ + + - +
+ ]
220 : 67 : const std::string filename = iter->path().filename().string();
221 : : char name[kFastBufferNameSize];
222 : 67 : uint64_t id = 0;
223 : :
224 [ + + ]: 67 : if (filename.size() >= kFastBufferNameSize - 1) {
225 : 28 : continue;
226 : : }
227 : :
228 : 39 : name[0] = '/';
229 : 39 : std::memcpy(name + 1, filename.c_str(), filename.size() + 1);
230 : :
231 [ + + ]: 39 : if (!parse_name(name, prefix, id)) {
232 : 33 : continue;
233 : : }
234 : :
235 : 6 : SysSharemem shared;
236 : :
237 [ + - ]: 6 : if (shared.attach(name)) {
238 : 6 : fn(shared);
239 : : }
240 : : }
241 : : #else
242 : : (void)prefix;
243 : : (void)fn;
244 : : #endif
245 : 14 : }
246 : :
247 : : // HostBufferPlugin
248 : : class HostBufferPlugin : public FastBufferPluginInterface {
249 : : public:
250 : 1 : uint64_t get_protocol_id() const noexcept override { return 0x564C484F53540001ULL; }
251 : :
252 : 25 : bool create(size_t size, const zerocopy::FastBuffer::Config& config,
253 : : zerocopy::FastBuffer::Buffer& buffer) noexcept override {
254 [ + - + + : 25 : if VUNLIKELY (size == 0 || config.device != -1 ||
+ + + + +
+ + + +
+ ]
255 : : (config.memory_type != zerocopy::FastBuffer::kMemoryDefault &&
256 : : config.memory_type != zerocopy::FastBuffer::kMemoryHost)) {
257 : 5 : return false;
258 : : }
259 : :
260 : 20 : auto* resource = pool_new<Bytes>();
261 : :
262 [ - + ]: 20 : if VUNLIKELY (!resource) {
263 : 0 : return false;
264 : : }
265 : :
266 : 20 : *resource = Bytes::create(size);
267 : :
268 [ - + ]: 20 : if VUNLIKELY (resource->empty()) {
269 : 0 : pool_delete(resource);
270 : 0 : return false;
271 : : }
272 : :
273 : 20 : buffer = {resource->data(), size, resource, -1, zerocopy::FastBuffer::kMemoryHost};
274 : 20 : return true;
275 : : }
276 : :
277 : 20 : bool release(zerocopy::FastBuffer::Buffer& buffer) noexcept override {
278 : 20 : pool_delete(static_cast<Bytes*>(buffer.handle));
279 : 20 : buffer = {};
280 : 20 : return true;
281 : : }
282 : :
283 : 59 : bool synchronize(const zerocopy::FastBuffer::Buffer&) noexcept override { return true; }
284 : :
285 : 2 : bool copy(const zerocopy::FastBuffer::Buffer& source,
286 : : const zerocopy::FastBuffer::Buffer& destination) noexcept override {
287 [ - + ]: 2 : if VUNLIKELY (source.size != destination.size) {
288 : 0 : return false;
289 : : }
290 : :
291 : 2 : std::memmove(destination.data, source.data, source.size);
292 : 2 : return true;
293 : : }
294 : :
295 : 12 : bool copy_to_host(const zerocopy::FastBuffer::Buffer& source, Bytes& destination) noexcept override {
296 [ - + ]: 12 : if VUNLIKELY (source.size != destination.size()) {
297 : 0 : return false;
298 : : }
299 : :
300 : 12 : std::memmove(destination.data(), source.data, source.size);
301 : 12 : return true;
302 : : }
303 : :
304 : 6 : bool copy_from_host(const Bytes& source, const zerocopy::FastBuffer::Buffer& destination) noexcept override {
305 [ - + ]: 6 : if VUNLIKELY (source.size() != destination.size) {
306 : 0 : return false;
307 : : }
308 : :
309 : 6 : std::memmove(destination.data, source.data(), source.size());
310 : 6 : return true;
311 : : }
312 : :
313 : 24 : uint8_t* map(const zerocopy::FastBuffer::Buffer& buffer, zerocopy::FastBuffer::Access access) noexcept override {
314 [ + + + + ]: 13 : return access == zerocopy::FastBuffer::kRead || access == zerocopy::FastBuffer::kWrite ||
315 : : access == zerocopy::FastBuffer::kReadWrite
316 [ + + ]: 37 : ? buffer.data
317 : 24 : : nullptr;
318 : : }
319 : :
320 : 24 : bool unmap(const zerocopy::FastBuffer::Buffer&, zerocopy::FastBuffer::Access access) noexcept override {
321 [ + + + + : 24 : return access == zerocopy::FastBuffer::kRead || access == zerocopy::FastBuffer::kWrite ||
+ + ]
322 : 24 : access == zerocopy::FastBuffer::kReadWrite;
323 : : }
324 : :
325 : 5 : const void* native_handle(const zerocopy::FastBuffer::Buffer& buffer) const noexcept override {
326 : 5 : return buffer.handle;
327 : : }
328 : : };
329 : :
330 : : // SharedHostBufferPlugin
331 : : class SharedHostBufferPlugin final : public HostBufferPlugin {
332 : : public:
333 : 6 : SharedHostBufferPlugin(uint64_t identity, uint64_t pid_namespace, uint64_t time_namespace) noexcept
334 : 6 : : identity_(identity), pid_namespace_(pid_namespace), time_namespace_(time_namespace) {
335 : 6 : sweep_segments(kFastBufferMemoryPrefix, [this](SysSharemem& shared) {
336 [ - + ]: 3 : if (shared.size() < kSegmentHeaderSize) {
337 : 0 : return;
338 : : }
339 : :
340 : 3 : const auto* header = static_cast<const std::atomic<uint64_t>*>(shared.data());
341 : 3 : const uint64_t owner = header[0].load(std::memory_order_acquire);
342 : :
343 [ + - ]: 6 : if (owner != 0 && header[1].load(std::memory_order_relaxed) == pid_namespace_ &&
344 [ + - + - : 9 : header[2].load(std::memory_order_relaxed) == time_namespace_ && !is_identity_alive(owner)) {
+ + + + ]
345 : 1 : (void)shared.detach();
346 : : }
347 : : });
348 : 6 : }
349 : :
350 : 43 : uint64_t get_protocol_id() const noexcept override { return 0x564C5348534D0001ULL; }
351 : :
352 : 8 : bool create(size_t size, const zerocopy::FastBuffer::Config& config,
353 : : zerocopy::FastBuffer::Buffer& buffer) noexcept override {
354 [ + - - + : 8 : if VUNLIKELY (size == 0 || size > std::numeric_limits<size_t>::max() - kSegmentHeaderSize || config.device != -1 ||
+ - - + +
- - + - -
- + - + ]
355 : : (config.memory_type != zerocopy::FastBuffer::kMemoryDefault &&
356 : : config.memory_type != zerocopy::FastBuffer::kMemoryShared)) {
357 : 0 : return false;
358 : : }
359 : :
360 : 8 : auto* segment = pool_new<Segment>();
361 : :
362 [ - + ]: 8 : if VUNLIKELY (!segment) {
363 : 0 : return false;
364 : : }
365 : :
366 : 8 : format_name(kFastBufferMemoryPrefix, generate_id(), segment->name);
367 : :
368 [ - + ]: 8 : if VUNLIKELY (!segment->shared.create(segment->name, kSegmentHeaderSize + size)) {
369 : 0 : pool_delete(segment);
370 : 0 : return false;
371 : : }
372 : :
373 : 8 : segment->owner = true;
374 : 8 : auto* base = static_cast<uint8_t*>(segment->shared.data());
375 [ + + ]: 32 : auto* header = new (base) std::atomic<uint64_t>[3]{};
376 : 8 : header[1].store(pid_namespace_, std::memory_order_relaxed);
377 : 8 : header[2].store(time_namespace_, std::memory_order_relaxed);
378 : 8 : header[0].store(identity_, std::memory_order_release);
379 : 8 : buffer = {base + kSegmentHeaderSize, size, segment, -1, zerocopy::FastBuffer::kMemoryShared};
380 : 8 : return true;
381 : : }
382 : :
383 : 13 : bool release(zerocopy::FastBuffer::Buffer& buffer) noexcept override {
384 : 13 : auto* segment = static_cast<Segment*>(buffer.handle);
385 : 13 : (void)segment->shared.detach(segment->owner);
386 : 13 : pool_delete(segment);
387 : 13 : buffer = {};
388 : 13 : return true;
389 : : }
390 : :
391 : 19 : size_t get_handle_size(const zerocopy::FastBuffer::Buffer&) const noexcept override { return sizeof(Descriptor); }
392 : :
393 : 7 : bool export_handle(const zerocopy::FastBuffer::Buffer& buffer, Bytes& descriptor) noexcept override {
394 [ - + ]: 7 : if VUNLIKELY (descriptor.size() != sizeof(Descriptor)) {
395 : 0 : return false;
396 : : }
397 : :
398 : 7 : Descriptor exported;
399 : 7 : std::memcpy(exported.name, static_cast<const Segment*>(buffer.handle)->name, sizeof(exported.name));
400 : 7 : exported.size = buffer.size;
401 : 7 : std::memcpy(descriptor.data(), &exported, sizeof(exported));
402 : 7 : return true;
403 : : }
404 : :
405 : 5 : bool import_handle(const Bytes& descriptor, zerocopy::FastBuffer::Buffer& buffer) noexcept override {
406 : 5 : Descriptor decoded;
407 : :
408 [ + - - + : 5 : if VUNLIKELY (!decode(descriptor, decoded) || decoded.size == 0) {
- + ]
409 : 0 : return false;
410 : : }
411 : :
412 : 5 : auto* segment = pool_new<Segment>();
413 : :
414 [ - + ]: 5 : if VUNLIKELY (!segment) {
415 : 0 : return false;
416 : : }
417 : :
418 [ + - - + : 5 : if VUNLIKELY (!segment->shared.attach(decoded.name) || segment->shared.size() < kSegmentHeaderSize ||
+ - - + +
- + - -
+ ]
419 : : segment->shared.size() - kSegmentHeaderSize < decoded.size) {
420 : 0 : pool_delete(segment);
421 : 0 : return false;
422 : : }
423 : :
424 : 5 : std::memcpy(segment->name, decoded.name, sizeof(segment->name));
425 : 5 : buffer = {static_cast<uint8_t*>(segment->shared.data()) + kSegmentHeaderSize, static_cast<size_t>(decoded.size),
426 : : segment, -1, zerocopy::FastBuffer::kMemoryShared};
427 : 5 : return true;
428 : : }
429 : :
430 : 2 : bool destroy_handle(const Bytes& descriptor) noexcept override {
431 : 2 : Descriptor decoded;
432 : :
433 [ - + ]: 2 : if VUNLIKELY (!decode(descriptor, decoded)) {
434 : 0 : return false;
435 : : }
436 : :
437 : 2 : SysSharemem shared;
438 [ + + + - : 2 : return shared.attach(decoded.name) && shared.detach();
+ - + - ]
439 : 2 : }
440 : :
441 : : private:
442 : : static constexpr size_t kSegmentHeaderSize = 64;
443 : :
444 : : struct Segment final {
445 : : char name[kFastBufferNameSize]{};
446 : : SysSharemem shared;
447 : : bool owner{false};
448 : : };
449 : :
450 : : struct Descriptor final {
451 : : char name[kFastBufferNameSize]{};
452 : : uint64_t size{0};
453 : : };
454 : :
455 : 7 : static bool decode(const Bytes& descriptor, Descriptor& decoded) noexcept {
456 [ - + ]: 7 : if VUNLIKELY (descriptor.size() != sizeof(Descriptor)) {
457 : 0 : return false;
458 : : }
459 : :
460 : 7 : std::memcpy(&decoded, descriptor.data(), sizeof(decoded));
461 : 7 : uint64_t id = 0;
462 : 7 : return parse_name(decoded.name, kFastBufferMemoryPrefix, id);
463 : : }
464 : :
465 : : uint64_t identity_{0};
466 : : uint64_t pid_namespace_{0};
467 : : uint64_t time_namespace_{0};
468 : : };
469 : :
470 : : // FastBufferControl
471 : : struct alignas(8) FastBufferControl final {
472 : : std::atomic<uint64_t> protocol{0};
473 : : uint64_t size{0};
474 : : uint64_t descriptor_size{0};
475 : : std::atomic<uint64_t> owner{0};
476 : : std::atomic<uint64_t> pid_namespace{0};
477 : : std::atomic<uint64_t> time_namespace{0};
478 : : std::atomic<uint64_t> generation{0};
479 : : std::atomic<uint64_t> readers[kFastBufferMaxReaders]{};
480 : : };
481 : :
482 : : // FastBufferSystemDescriptor
483 : : struct FastBufferSystemDescriptor final {
484 : : char name[kFastBufferNameSize]{};
485 : : uint64_t generation{0};
486 : : };
487 : :
488 : : static constexpr size_t kFastBufferHandleSize = sizeof(FastBufferSystemDescriptor);
489 : :
490 : : static_assert(std::atomic<uint64_t>::is_always_lock_free, "Shared control blocks require lock-free atomics.");
491 : :
492 : : // FastBufferShare
493 : : struct FastBufferShare final {
494 : 15 : ~FastBufferShare() {
495 [ + + + - : 15 : if (owner && shared.is_attached()) {
+ + ]
496 : 7 : (void)shared.detach();
497 : : }
498 : 15 : }
499 : :
500 : : uint64_t id{0};
501 : : uint64_t generation{0};
502 : : SysSharemem shared;
503 : : FastBufferControl* control{nullptr};
504 : : size_t reader{0};
505 : : bool owner{false};
506 : : };
507 : :
508 : : // FastBufferResource
509 : : struct FastBufferResource final {
510 : 42 : ~FastBufferResource() { pool_delete(share.load(std::memory_order_acquire)); }
511 : :
512 : : zerocopy::FastBuffer::Buffer native;
513 : : std::atomic<size_t> references{1};
514 : : std::atomic<FastBufferShare*> share{nullptr};
515 : : FastBufferResource* next{nullptr};
516 : : };
517 : :
518 : : enum class LocalImport : uint8_t {
519 : : kMissing = 0,
520 : : kImported = 1,
521 : : kExpired = 2,
522 : : };
523 : :
524 : : // FastBufferManager::Impl
525 : : struct FastBufferManager::Impl final {
526 : 20 : [[nodiscard]] std::mutex& stripe_of(uint64_t id) noexcept { return stripes[id % stripes.size()]; }
527 : :
528 : : void free_resource(FastBufferResource* resource) noexcept;
529 : :
530 : : void retire_owner(FastBufferResource* resource) noexcept;
531 : :
532 : : void reclaim_retired() noexcept;
533 : :
534 : : [[nodiscard]] LocalImport import_local(uint64_t id, uint64_t generation,
535 : : zerocopy::FastBuffer::Buffer& buffer) noexcept;
536 : :
537 : : void destroy_dead_owner(const FastBufferControl& control, SysSharemem& shared) noexcept;
538 : :
539 : : Plugin plugin;
540 : : std::shared_ptr<FastBufferPluginInterface> interface;
541 : : uint64_t identity{0};
542 : : uint64_t pid_namespace{0};
543 : : uint64_t time_namespace{0};
544 : : std::mutex mutex;
545 : : std::unordered_map<uint64_t, FastBufferResource*, std::hash<uint64_t>, std::equal_to<>,
546 : : PoolAllocator<std::pair<const uint64_t, FastBufferResource*>>>
547 : : resources;
548 : : std::atomic<FastBufferResource*> retired{nullptr};
549 : : std::array<std::mutex, 64> stripes;
550 : : };
551 : :
552 : 19 : static bool purge_readers(FastBufferControl& control) noexcept {
553 : 19 : bool busy = false;
554 : :
555 [ + + ]: 627 : for (auto& reader : control.readers) {
556 : 608 : uint64_t identity = reader.load(std::memory_order_seq_cst);
557 : :
558 [ + + ]: 608 : while (identity != 0) {
559 [ + + ]: 10 : if (is_identity_alive(identity)) {
560 : 9 : busy = true;
561 : 9 : break;
562 : : }
563 : :
564 [ + - ]: 2 : if (reader.compare_exchange_strong(identity, 0, std::memory_order_seq_cst)) {
565 : 1 : break;
566 : : }
567 : : }
568 : : }
569 : :
570 : 19 : return !busy;
571 : : }
572 : :
573 : 7 : void FastBufferManager::Impl::free_resource(FastBufferResource* resource) noexcept {
574 [ - + ]: 7 : if VUNLIKELY (!interface->release(resource->native)) {
575 [ # # ]: 0 : VLOG_E("FastBufferManager: Keeping resource after SDK release failure.");
576 : 0 : return;
577 : : }
578 : :
579 : 7 : pool_delete(resource);
580 : : }
581 : :
582 : 7 : void FastBufferManager::Impl::retire_owner(FastBufferResource* resource) noexcept {
583 [ + + ]: 7 : if (purge_readers(*resource->share.load(std::memory_order_acquire)->control)) {
584 : 5 : free_resource(resource);
585 : 5 : return;
586 : : }
587 : :
588 : 2 : std::lock_guard lock(mutex);
589 : 2 : resource->next = retired.load(std::memory_order_relaxed);
590 : 2 : retired.store(resource, std::memory_order_relaxed);
591 : 2 : }
592 : :
593 : 75 : void FastBufferManager::Impl::reclaim_retired() noexcept {
594 [ + + ]: 75 : if VLIKELY (retired.load(std::memory_order_relaxed) == nullptr) {
595 : 70 : return;
596 : : }
597 : :
598 : 5 : FastBufferResource* pending = nullptr;
599 : :
600 : : {
601 : 5 : std::lock_guard lock(mutex);
602 : 5 : pending = retired.exchange(nullptr, std::memory_order_relaxed);
603 : 5 : }
604 : :
605 : 5 : FastBufferResource* busy = nullptr;
606 : :
607 [ + + ]: 10 : while (pending) {
608 : 5 : auto* resource = pending;
609 : 5 : pending = resource->next;
610 : 5 : resource->next = nullptr;
611 : :
612 [ + + ]: 5 : if (purge_readers(*resource->share.load(std::memory_order_acquire)->control)) {
613 : 1 : free_resource(resource);
614 : : } else {
615 : 4 : resource->next = busy;
616 : 4 : busy = resource;
617 : : }
618 : : }
619 : :
620 [ + + ]: 5 : if (busy) {
621 : 4 : auto* tail = busy;
622 : :
623 [ - + ]: 4 : while (tail->next) {
624 : 0 : tail = tail->next;
625 : : }
626 : :
627 : 4 : std::lock_guard lock(mutex);
628 : 4 : tail->next = retired.load(std::memory_order_relaxed);
629 : 4 : retired.store(busy, std::memory_order_relaxed);
630 : 4 : }
631 : : }
632 : :
633 : 23 : LocalImport FastBufferManager::Impl::import_local(uint64_t id, uint64_t generation,
634 : : zerocopy::FastBuffer::Buffer& buffer) noexcept {
635 : 23 : std::lock_guard lock(mutex);
636 : 23 : const auto iter = resources.find(id);
637 : :
638 [ + + ]: 23 : if (iter == resources.end()) {
639 : 16 : return LocalImport::kMissing;
640 : : }
641 : :
642 : 7 : auto* resource = iter->second;
643 : 7 : const auto* share = resource->share.load(std::memory_order_acquire);
644 : :
645 [ + + ]: 14 : if VUNLIKELY (share->control->generation.load(std::memory_order_acquire) != generation) {
646 : 3 : return LocalImport::kExpired;
647 : : }
648 : :
649 : 4 : resource->references.fetch_add(1, std::memory_order_relaxed);
650 : 4 : buffer = resource->native;
651 : 4 : buffer.handle = resource;
652 : 4 : return LocalImport::kImported;
653 : 23 : }
654 : :
655 : 2 : void FastBufferManager::Impl::destroy_dead_owner(const FastBufferControl& control, SysSharemem& shared) noexcept {
656 [ + - + - : 2 : if (control.descriptor_size != 0 && control.descriptor_size <= shared.size() - sizeof(FastBufferControl)) {
+ - ]
657 : 4 : (void)interface->destroy_handle(Bytes::shallow_copy(reinterpret_cast<const uint8_t*>(&control + 1),
658 : 2 : static_cast<size_t>(control.descriptor_size)));
659 : : }
660 : :
661 : 2 : (void)shared.detach();
662 : 2 : }
663 : :
664 : : // FastBufferManager
665 : 300 : FastBufferManager& FastBufferManager::get() {
666 [ + + + - : 300 : static FastBufferManager global_fast_buffer;
+ - - - ]
667 : 300 : return global_fast_buffer;
668 : : }
669 : :
670 : 5 : bool FastBufferManager::is_valid() const { return impl_->interface != nullptr; }
671 : :
672 : 151 : FastBufferPluginInterface* FastBufferManager::get_interface() const { return impl_->interface.get(); }
673 : :
674 : 101 : const zerocopy::FastBuffer::Buffer& FastBufferManager::native_buffer(
675 : : const zerocopy::FastBuffer::Buffer& buffer) noexcept {
676 : 101 : return static_cast<const FastBufferResource*>(buffer.handle)->native;
677 : : }
678 : :
679 : 33 : bool FastBufferManager::create(size_t size, const zerocopy::FastBuffer::Config& config,
680 : : zerocopy::FastBuffer::Buffer& buffer) noexcept {
681 : 33 : impl_->reclaim_retired();
682 : :
683 : 33 : PoolPtr<FastBufferResource> resource(pool_new<FastBufferResource>());
684 : :
685 [ + - + + : 33 : if VUNLIKELY (!resource || !impl_->interface->create(size, config, resource->native)) {
+ + ]
686 : 5 : return false;
687 : : }
688 : :
689 : 28 : buffer = resource->native;
690 : 28 : buffer.handle = resource.release();
691 : 28 : return true;
692 : 33 : }
693 : :
694 : 13 : void FastBufferManager::retain(const zerocopy::FastBuffer::Buffer& source,
695 : : zerocopy::FastBuffer::Buffer& buffer) noexcept {
696 : 13 : auto* resource = static_cast<FastBufferResource*>(source.handle);
697 : 13 : resource->references.fetch_add(1, std::memory_order_relaxed);
698 : 13 : buffer = source;
699 : 13 : }
700 : :
701 : 50 : void FastBufferManager::release(zerocopy::FastBuffer::Buffer& buffer) noexcept {
702 : 50 : auto* resource = static_cast<FastBufferResource*>(buffer.handle);
703 : 50 : buffer = {};
704 : 50 : size_t references = resource->references.load(std::memory_order_acquire);
705 : :
706 [ + + ]: 50 : while (references > 1) {
707 [ + - ]: 34 : if (resource->references.compare_exchange_weak(references, references - 1, std::memory_order_acq_rel,
708 : : std::memory_order_acquire)) {
709 : 24 : return;
710 : : }
711 : : }
712 : :
713 : 33 : impl_->reclaim_retired();
714 : :
715 : 33 : auto* share = resource->share.load(std::memory_order_acquire);
716 [ + + ]: 33 : std::unique_lock stripe(share ? impl_->stripe_of(share->id) : impl_->stripes[0], std::defer_lock);
717 : 33 : std::unique_lock registry(impl_->mutex, std::defer_lock);
718 : :
719 [ + + ]: 33 : if (share) {
720 [ + + ]: 12 : if (!share->owner) {
721 : 5 : stripe.lock();
722 : : }
723 : :
724 : 12 : registry.lock();
725 : : }
726 : :
727 [ - + ]: 66 : if (resource->references.fetch_sub(1, std::memory_order_acq_rel) != 1) {
728 : 0 : return;
729 : : }
730 : :
731 [ + + ]: 33 : if (share) {
732 : 12 : impl_->resources.erase(share->id);
733 : :
734 [ + + ]: 12 : if (share->owner) {
735 : 7 : share->control->generation.fetch_add(1, std::memory_order_seq_cst);
736 : : }
737 : :
738 : 12 : registry.unlock();
739 : : }
740 : :
741 [ - + ]: 33 : if VUNLIKELY (!impl_->interface->synchronize(resource->native)) {
742 [ # # ]: 0 : VLOG_E("FastBufferManager: Keeping resource after synchronization failure.");
743 : 0 : return;
744 : : }
745 : :
746 [ + + + + ]: 33 : if (share && share->owner) {
747 : 7 : impl_->retire_owner(resource);
748 : 7 : return;
749 : : }
750 : :
751 [ - + ]: 26 : if VUNLIKELY (!impl_->interface->release(resource->native)) {
752 [ # # ]: 0 : VLOG_E("FastBufferManager: Keeping resource after SDK release failure.");
753 : 0 : return;
754 : : }
755 : :
756 [ + + ]: 26 : if (share) {
757 : 5 : share->control->readers[share->reader].store(0, std::memory_order_release);
758 : : }
759 : :
760 : 26 : pool_delete(resource);
761 [ + + + + ]: 40 : }
762 : :
763 : 21 : bool FastBufferManager::reclaim(const zerocopy::FastBuffer::Buffer& buffer) noexcept {
764 : 21 : auto* resource = static_cast<FastBufferResource*>(buffer.handle);
765 : :
766 [ + + ]: 42 : if (resource->references.load(std::memory_order_acquire) != 1) {
767 : 7 : return false;
768 : : }
769 : :
770 : 14 : const auto* share = resource->share.load(std::memory_order_acquire);
771 : :
772 [ + + ]: 14 : if (!share) {
773 : 7 : return impl_->interface->synchronize(resource->native);
774 : : }
775 : :
776 : 7 : auto& control = *share->control;
777 : :
778 [ + + ]: 7 : if (!purge_readers(control)) {
779 : 3 : return false;
780 : : }
781 : :
782 : : {
783 : 4 : std::lock_guard lock(impl_->mutex);
784 : :
785 [ - + ]: 8 : if (resource->references.load(std::memory_order_relaxed) != 1) {
786 : 0 : return false;
787 : : }
788 : :
789 : 4 : control.generation.fetch_add(1, std::memory_order_seq_cst);
790 [ + - ]: 4 : }
791 : :
792 [ + + ]: 132 : for (const auto& reader : control.readers) {
793 [ - + ]: 256 : if (reader.load(std::memory_order_seq_cst) != 0) {
794 : 0 : return false;
795 : : }
796 : : }
797 : :
798 : 4 : return impl_->interface->synchronize(resource->native);
799 : : }
800 : :
801 : 1 : bool FastBufferManager::import_native(const void* handle, zerocopy::FastBuffer::Buffer& buffer) noexcept {
802 : 1 : PoolPtr<FastBufferResource> resource(pool_new<FastBufferResource>());
803 : :
804 [ + - + - : 1 : if VUNLIKELY (!resource || !impl_->interface->import_native(handle, resource->native)) {
+ - ]
805 : 1 : return false;
806 : : }
807 : :
808 : 0 : buffer = resource->native;
809 : 0 : buffer.handle = resource.release();
810 : 0 : return true;
811 : 1 : }
812 : :
813 : 22 : size_t FastBufferManager::get_handle_size(const zerocopy::FastBuffer::Buffer& buffer) const noexcept {
814 : : #if defined(__ANDROID__)
815 : : (void)buffer;
816 : : return 0;
817 : : #else
818 : 22 : const size_t size = impl_->interface->get_handle_size(native_buffer(buffer));
819 [ + + - + ]: 22 : return size == 0 || impl_->identity == 0 ? 0 : kFastBufferHandleSize;
820 : : #endif
821 : : }
822 : :
823 : 11 : bool FastBufferManager::export_handle(const zerocopy::FastBuffer::Buffer& buffer, Bytes& descriptor) noexcept {
824 : 11 : auto* resource = static_cast<FastBufferResource*>(buffer.handle);
825 : :
826 [ - + ]: 11 : if VUNLIKELY (!impl_->interface->synchronize(resource->native)) {
827 : 0 : return false;
828 : : }
829 : :
830 : 11 : auto* share = resource->share.load(std::memory_order_acquire);
831 : :
832 [ + + ]: 11 : if (!share) {
833 : 7 : std::lock_guard stripe(impl_->stripes[std::hash<const void*>{}(resource) % impl_->stripes.size()]);
834 : 7 : share = resource->share.load(std::memory_order_acquire);
835 : :
836 [ + - ]: 7 : if (!share) {
837 : 7 : const size_t size = impl_->interface->get_handle_size(resource->native);
838 : :
839 [ - + ]: 7 : if VUNLIKELY (size == 0) {
840 : 0 : return false;
841 : : }
842 : :
843 : 7 : PoolPtr<FastBufferShare> created(pool_new<FastBufferShare>());
844 : :
845 [ - + ]: 7 : if VUNLIKELY (!created) {
846 : 0 : return false;
847 : : }
848 : :
849 : 7 : created->id = generate_id();
850 : : char name[kFastBufferNameSize];
851 : 7 : format_name(kFastBufferControlPrefix, created->id, name);
852 : :
853 [ - + ]: 7 : if VUNLIKELY (!created->shared.create(name, sizeof(FastBufferControl) + size)) {
854 : 0 : return false;
855 : : }
856 : :
857 : 7 : created->owner = true;
858 : 7 : created->control = new (created->shared.data()) FastBufferControl;
859 : 7 : created->control->protocol.store(impl_->interface->get_protocol_id(), std::memory_order_relaxed);
860 : 7 : created->control->size = resource->native.size;
861 : 7 : created->control->descriptor_size = size;
862 : 7 : created->control->pid_namespace.store(impl_->pid_namespace, std::memory_order_relaxed);
863 : 7 : created->control->time_namespace.store(impl_->time_namespace, std::memory_order_relaxed);
864 : 7 : created->control->generation.store(1, std::memory_order_relaxed);
865 : 7 : created->control->owner.store(impl_->identity, std::memory_order_release);
866 : 7 : auto native_descriptor = Bytes::shallow_copy(reinterpret_cast<uint8_t*>(created->control + 1), size);
867 : :
868 [ - + ]: 7 : if VUNLIKELY (!impl_->interface->export_handle(resource->native, native_descriptor)) {
869 : 0 : return false;
870 : : }
871 : :
872 : 7 : share = created.release();
873 : :
874 : : {
875 : 7 : std::lock_guard registry(impl_->mutex);
876 : 7 : impl_->resources.emplace(share->id, resource);
877 : 7 : }
878 : :
879 : 7 : resource->share.store(share, std::memory_order_release);
880 [ + - + - ]: 7 : }
881 [ + - ]: 7 : }
882 : :
883 : 11 : FastBufferSystemDescriptor exported;
884 : 11 : format_name(kFastBufferControlPrefix, share->id, exported.name);
885 [ + + ]: 20 : exported.generation = share->owner ? share->control->generation.load(std::memory_order_acquire) : share->generation;
886 : 11 : std::memcpy(descriptor.data(), &exported, sizeof(exported));
887 : 11 : return true;
888 : : }
889 : :
890 : 15 : bool FastBufferManager::import_handle(const Bytes& descriptor, zerocopy::FastBuffer::Buffer& buffer) noexcept {
891 : : #if defined(__ANDROID__)
892 : : (void)descriptor;
893 : : (void)buffer;
894 : : return false;
895 : : #else
896 [ + - - + : 15 : if VUNLIKELY (descriptor.size() != kFastBufferHandleSize || impl_->identity == 0) {
- + ]
897 : 0 : return false;
898 : : }
899 : :
900 : 15 : FastBufferSystemDescriptor decoded;
901 : 15 : std::memcpy(&decoded, descriptor.data(), sizeof(decoded));
902 : 15 : uint64_t id = 0;
903 : :
904 [ + - - + : 15 : if VUNLIKELY (!parse_name(decoded.name, kFastBufferControlPrefix, id) || decoded.generation == 0) {
- + ]
905 : 0 : return false;
906 : : }
907 : :
908 : 15 : LocalImport local = impl_->import_local(id, decoded.generation, buffer);
909 : :
910 [ + + ]: 15 : if (local != LocalImport::kMissing) {
911 : 7 : return local == LocalImport::kImported;
912 : : }
913 : :
914 : 8 : std::lock_guard stripe(impl_->stripe_of(id));
915 : 8 : local = impl_->import_local(id, decoded.generation, buffer);
916 : :
917 [ - + ]: 8 : if (local != LocalImport::kMissing) {
918 : 0 : return local == LocalImport::kImported;
919 : : }
920 : :
921 : 8 : PoolPtr<FastBufferResource> resource(pool_new<FastBufferResource>());
922 : 8 : PoolPtr<FastBufferShare> share(pool_new<FastBufferShare>());
923 : :
924 [ + - - + : 8 : if VUNLIKELY (!resource || !share) {
- + ]
925 : 0 : return false;
926 : : }
927 : :
928 [ + + - + : 8 : if VUNLIKELY (!share->shared.attach(decoded.name) || share->shared.size() <= sizeof(FastBufferControl)) {
+ - + - +
+ ]
929 : 1 : return false;
930 : : }
931 : :
932 : 7 : share->control = static_cast<FastBufferControl*>(share->shared.data());
933 : 7 : auto& control = *share->control;
934 : :
935 : 7 : const uint64_t owner = control.owner.load(std::memory_order_acquire);
936 : :
937 [ + - - + : 28 : if VUNLIKELY (owner == 0 || control.protocol.load(std::memory_order_relaxed) != impl_->interface->get_protocol_id() ||
+ - - + +
- - + + -
- + + - -
+ + - - +
- + ]
938 : : control.pid_namespace.load(std::memory_order_relaxed) != impl_->pid_namespace ||
939 : : control.time_namespace.load(std::memory_order_relaxed) != impl_->time_namespace || control.size == 0 ||
940 : : control.descriptor_size == 0 ||
941 : : control.descriptor_size > share->shared.size() - sizeof(FastBufferControl)) {
942 : 0 : return false;
943 : : }
944 : :
945 [ + + ]: 7 : if VUNLIKELY (!is_identity_alive(owner)) {
946 : 1 : impl_->destroy_dead_owner(control, share->shared);
947 : 1 : return false;
948 : : }
949 : :
950 : 6 : size_t index = 0;
951 : :
952 [ + - ]: 6 : for (; index < kFastBufferMaxReaders; ++index) {
953 : 6 : uint64_t expected = 0;
954 : :
955 [ + - ]: 12 : if (control.readers[index].compare_exchange_strong(expected, impl_->identity, std::memory_order_seq_cst)) {
956 : 6 : break;
957 : : }
958 : : }
959 : :
960 [ - + ]: 6 : if VUNLIKELY (index == kFastBufferMaxReaders) {
961 [ # # ]: 0 : VLOG_E("FastBufferManager: Reader table of the shared allocation is full.");
962 : 0 : return false;
963 : : }
964 : :
965 : 6 : auto& reader = control.readers[index];
966 : :
967 [ + + ]: 12 : if VUNLIKELY (control.generation.load(std::memory_order_seq_cst) != decoded.generation) {
968 : 1 : reader.store(0, std::memory_order_release);
969 : 1 : return false;
970 : : }
971 : :
972 : 10 : const auto native_descriptor = Bytes::shallow_copy(reinterpret_cast<const uint8_t*>(share->control + 1),
973 : 5 : static_cast<size_t>(control.descriptor_size));
974 : :
975 [ - + ]: 5 : if VUNLIKELY (!impl_->interface->import_handle(native_descriptor, resource->native)) {
976 : 0 : reader.store(0, std::memory_order_release);
977 : 0 : return false;
978 : : }
979 : :
980 [ - + ]: 5 : if VUNLIKELY (resource->native.size != control.size) {
981 [ # # ]: 0 : if (impl_->interface->release(resource->native)) {
982 : 0 : reader.store(0, std::memory_order_release);
983 : : } else {
984 [ # # ]: 0 : VLOG_E("FastBufferManager: Leaking rejected import after SDK release failure.");
985 : : }
986 : :
987 : 0 : return false;
988 : : }
989 : :
990 : 5 : share->id = id;
991 : 5 : share->generation = decoded.generation;
992 : 5 : share->reader = index;
993 : 5 : resource->share.store(share.release(), std::memory_order_release);
994 : :
995 : : {
996 : 5 : std::lock_guard registry(impl_->mutex);
997 : 5 : impl_->resources.emplace(id, resource.get());
998 : 5 : }
999 : :
1000 : 5 : buffer = resource->native;
1001 : 5 : buffer.handle = resource.release();
1002 : 5 : return true;
1003 : : #endif
1004 : 8 : }
1005 : :
1006 : 9 : FastBufferManager::FastBufferManager() : impl_(std::make_unique<Impl>()) {
1007 [ + - ]: 9 : (void)MemoryPool::global_instance();
1008 : 9 : impl_->identity = process_identity(Utils::get_pid());
1009 : 9 : impl_->pid_namespace = namespace_inode("/proc/self/ns/pid");
1010 : 9 : impl_->time_namespace = namespace_inode("/proc/self/ns/time");
1011 : :
1012 [ - + ]: 9 : if VUNLIKELY (impl_->identity == 0) {
1013 [ # # # # : 0 : VLOG_W("FastBufferManager: Process identity is unavailable; descriptor sharing is disabled.");
# # ]
1014 : : }
1015 : :
1016 [ + - + - ]: 18 : const std::string plugin_name = Utils::get_env("VLINK_FASTBUFFER_PLUGIN");
1017 : :
1018 [ + + ]: 9 : if (plugin_name.empty()) {
1019 [ + - ]: 2 : impl_->interface = std::make_shared<HostBufferPlugin>();
1020 [ + + ]: 7 : } else if (plugin_name == "shm") {
1021 : 6 : impl_->interface =
1022 [ + - ]: 12 : std::make_shared<SharedHostBufferPlugin>(impl_->identity, impl_->pid_namespace, impl_->time_namespace);
1023 : : } else {
1024 [ + - + - : 1 : impl_->interface = impl_->plugin.load<FastBufferPluginInterface>(plugin_name, 1, 0);
+ - + - ]
1025 : :
1026 [ - + - - : 1 : if VUNLIKELY (impl_->interface && impl_->interface->get_protocol_id() == 0) {
- + ]
1027 [ # # # # : 0 : VLOG_E("FastBufferManager: Plugin descriptor protocol ID must not be zero.");
# # ]
1028 : 0 : impl_->interface.reset();
1029 : : }
1030 : : }
1031 : :
1032 [ + + + - : 9 : if (impl_->interface && impl_->identity != 0) {
+ + ]
1033 : 8 : sweep_segments(kFastBufferControlPrefix, [this](SysSharemem& shared) {
1034 [ - + ]: 3 : if (shared.size() <= sizeof(FastBufferControl)) {
1035 : 0 : return;
1036 : : }
1037 : :
1038 : 3 : const auto& control = *static_cast<const FastBufferControl*>(shared.data());
1039 : 3 : const uint64_t owner = control.owner.load(std::memory_order_acquire);
1040 : :
1041 [ + - ]: 6 : if (owner != 0 && control.protocol.load(std::memory_order_relaxed) == impl_->interface->get_protocol_id() &&
1042 [ + - ]: 6 : control.pid_namespace.load(std::memory_order_relaxed) == impl_->pid_namespace &&
1043 [ + - + - : 12 : control.time_namespace.load(std::memory_order_relaxed) == impl_->time_namespace &&
+ + ]
1044 [ + + ]: 3 : !is_identity_alive(owner)) {
1045 : 1 : impl_->destroy_dead_owner(control, shared);
1046 : : }
1047 : : });
1048 : : }
1049 : 9 : }
1050 : :
1051 : 9 : FastBufferManager::~FastBufferManager() {
1052 : : #ifdef _WIN32
1053 : : if (Utils::is_terminating()) {
1054 : : (void)impl_.release();
1055 : : return;
1056 : : }
1057 : : #endif
1058 : :
1059 : 9 : impl_->reclaim_retired();
1060 : 9 : auto* pending = impl_->retired.exchange(nullptr, std::memory_order_relaxed);
1061 : :
1062 [ + + ]: 10 : while (pending) {
1063 : 1 : auto* resource = pending;
1064 : 1 : pending = resource->next;
1065 [ + - ]: 2 : VLOG_W("FastBufferManager: Releasing a retired allocation still imported by a live process at shutdown.");
1066 : 1 : impl_->free_resource(resource);
1067 : : }
1068 : :
1069 : 9 : impl_->interface.reset();
1070 : 9 : impl_->plugin.clear();
1071 : 9 : }
1072 : :
1073 : : } // namespace vlink
|