1 // Copyright 2011 Google Inc. All Rights Reserved. 2 // 3 // Use of this source code is governed by a BSD-style license 4 // that can be found in the COPYING file in the root of the source 5 // tree. An additional intellectual property rights grant can be found 6 // in the file PATENTS. All contributing project authors may 7 // be found in the AUTHORS file in the root of the source tree. 8 // ----------------------------------------------------------------------------- 9 // 10 // Internal objects and utils for mux. 11 // 12 // Authors: Urvang (urvang (at) google.com) 13 // Vikas (vikasa (at) google.com) 14 15 #include <assert.h> 16 #include "./muxi.h" 17 #include "../utils/utils.h" 18 19 #if defined(__cplusplus) || defined(c_plusplus) 20 extern "C" { 21 #endif 22 23 #define UNDEFINED_CHUNK_SIZE (-1) 24 25 const ChunkInfo kChunks[] = { 26 { MKFOURCC('V', 'P', '8', 'X'), WEBP_CHUNK_VP8X, VP8X_CHUNK_SIZE }, 27 { MKFOURCC('I', 'C', 'C', 'P'), WEBP_CHUNK_ICCP, UNDEFINED_CHUNK_SIZE }, 28 { MKFOURCC('A', 'N', 'I', 'M'), WEBP_CHUNK_ANIM, ANIM_CHUNK_SIZE }, 29 { MKFOURCC('A', 'N', 'M', 'F'), WEBP_CHUNK_ANMF, ANMF_CHUNK_SIZE }, 30 { MKFOURCC('F', 'R', 'G', 'M'), WEBP_CHUNK_FRGM, FRGM_CHUNK_SIZE }, 31 { MKFOURCC('A', 'L', 'P', 'H'), WEBP_CHUNK_ALPHA, UNDEFINED_CHUNK_SIZE }, 32 { MKFOURCC('V', 'P', '8', ' '), WEBP_CHUNK_IMAGE, UNDEFINED_CHUNK_SIZE }, 33 { MKFOURCC('V', 'P', '8', 'L'), WEBP_CHUNK_IMAGE, UNDEFINED_CHUNK_SIZE }, 34 { MKFOURCC('E', 'X', 'I', 'F'), WEBP_CHUNK_EXIF, UNDEFINED_CHUNK_SIZE }, 35 { MKFOURCC('X', 'M', 'P', ' '), WEBP_CHUNK_XMP, UNDEFINED_CHUNK_SIZE }, 36 { MKFOURCC('U', 'N', 'K', 'N'), WEBP_CHUNK_UNKNOWN, UNDEFINED_CHUNK_SIZE }, 37 38 { NIL_TAG, WEBP_CHUNK_NIL, UNDEFINED_CHUNK_SIZE } 39 }; 40 41 //------------------------------------------------------------------------------ 42 43 int WebPGetMuxVersion(void) { 44 return (MUX_MAJ_VERSION << 16) | (MUX_MIN_VERSION << 8) | MUX_REV_VERSION; 45 } 46 47 //------------------------------------------------------------------------------ 48 // Life of a chunk object. 49 50 void ChunkInit(WebPChunk* const chunk) { 51 assert(chunk); 52 memset(chunk, 0, sizeof(*chunk)); 53 chunk->tag_ = NIL_TAG; 54 } 55 56 WebPChunk* ChunkRelease(WebPChunk* const chunk) { 57 WebPChunk* next; 58 if (chunk == NULL) return NULL; 59 if (chunk->owner_) { 60 WebPDataClear(&chunk->data_); 61 } 62 next = chunk->next_; 63 ChunkInit(chunk); 64 return next; 65 } 66 67 //------------------------------------------------------------------------------ 68 // Chunk misc methods. 69 70 CHUNK_INDEX ChunkGetIndexFromTag(uint32_t tag) { 71 int i; 72 for (i = 0; kChunks[i].tag != NIL_TAG; ++i) { 73 if (tag == kChunks[i].tag) return i; 74 } 75 return IDX_NIL; 76 } 77 78 WebPChunkId ChunkGetIdFromTag(uint32_t tag) { 79 int i; 80 for (i = 0; kChunks[i].tag != NIL_TAG; ++i) { 81 if (tag == kChunks[i].tag) return kChunks[i].id; 82 } 83 return WEBP_CHUNK_NIL; 84 } 85 86 uint32_t ChunkGetTagFromFourCC(const char fourcc[4]) { 87 return MKFOURCC(fourcc[0], fourcc[1], fourcc[2], fourcc[3]); 88 } 89 90 CHUNK_INDEX ChunkGetIndexFromFourCC(const char fourcc[4]) { 91 const uint32_t tag = ChunkGetTagFromFourCC(fourcc); 92 const CHUNK_INDEX idx = ChunkGetIndexFromTag(tag); 93 return (idx == IDX_NIL) ? IDX_UNKNOWN : idx; 94 } 95 96 //------------------------------------------------------------------------------ 97 // Chunk search methods. 98 99 // Returns next chunk in the chunk list with the given tag. 100 static WebPChunk* ChunkSearchNextInList(WebPChunk* chunk, uint32_t tag) { 101 while (chunk != NULL && chunk->tag_ != tag) { 102 chunk = chunk->next_; 103 } 104 return chunk; 105 } 106 107 WebPChunk* ChunkSearchList(WebPChunk* first, uint32_t nth, uint32_t tag) { 108 uint32_t iter = nth; 109 first = ChunkSearchNextInList(first, tag); 110 if (first == NULL) return NULL; 111 112 while (--iter != 0) { 113 WebPChunk* next_chunk = ChunkSearchNextInList(first->next_, tag); 114 if (next_chunk == NULL) break; 115 first = next_chunk; 116 } 117 return ((nth > 0) && (iter > 0)) ? NULL : first; 118 } 119 120 // Outputs a pointer to 'prev_chunk->next_', 121 // where 'prev_chunk' is the pointer to the chunk at position (nth - 1). 122 // Returns true if nth chunk was found. 123 static int ChunkSearchListToSet(WebPChunk** chunk_list, uint32_t nth, 124 WebPChunk*** const location) { 125 uint32_t count = 0; 126 assert(chunk_list != NULL); 127 *location = chunk_list; 128 129 while (*chunk_list != NULL) { 130 WebPChunk* const cur_chunk = *chunk_list; 131 ++count; 132 if (count == nth) return 1; // Found. 133 chunk_list = &cur_chunk->next_; 134 *location = chunk_list; 135 } 136 137 // *chunk_list is ok to be NULL if adding at last location. 138 return (nth == 0 || (count == nth - 1)) ? 1 : 0; 139 } 140 141 //------------------------------------------------------------------------------ 142 // Chunk writer methods. 143 144 WebPMuxError ChunkAssignData(WebPChunk* chunk, const WebPData* const data, 145 int copy_data, uint32_t tag) { 146 // For internally allocated chunks, always copy data & make it owner of data. 147 if (tag == kChunks[IDX_VP8X].tag || tag == kChunks[IDX_ANIM].tag) { 148 copy_data = 1; 149 } 150 151 ChunkRelease(chunk); 152 153 if (data != NULL) { 154 if (copy_data) { // Copy data. 155 if (!WebPDataCopy(data, &chunk->data_)) return WEBP_MUX_MEMORY_ERROR; 156 chunk->owner_ = 1; // Chunk is owner of data. 157 } else { // Don't copy data. 158 chunk->data_ = *data; 159 } 160 } 161 chunk->tag_ = tag; 162 return WEBP_MUX_OK; 163 } 164 165 WebPMuxError ChunkSetNth(WebPChunk* chunk, WebPChunk** chunk_list, 166 uint32_t nth) { 167 WebPChunk* new_chunk; 168 169 if (!ChunkSearchListToSet(chunk_list, nth, &chunk_list)) { 170 return WEBP_MUX_NOT_FOUND; 171 } 172 173 new_chunk = (WebPChunk*)malloc(sizeof(*new_chunk)); 174 if (new_chunk == NULL) return WEBP_MUX_MEMORY_ERROR; 175 *new_chunk = *chunk; 176 chunk->owner_ = 0; 177 new_chunk->next_ = *chunk_list; 178 *chunk_list = new_chunk; 179 return WEBP_MUX_OK; 180 } 181 182 //------------------------------------------------------------------------------ 183 // Chunk deletion method(s). 184 185 WebPChunk* ChunkDelete(WebPChunk* const chunk) { 186 WebPChunk* const next = ChunkRelease(chunk); 187 free(chunk); 188 return next; 189 } 190 191 //------------------------------------------------------------------------------ 192 // Chunk serialization methods. 193 194 size_t ChunksListDiskSize(const WebPChunk* chunk_list) { 195 size_t size = 0; 196 while (chunk_list != NULL) { 197 size += ChunkDiskSize(chunk_list); 198 chunk_list = chunk_list->next_; 199 } 200 return size; 201 } 202 203 static uint8_t* ChunkEmit(const WebPChunk* const chunk, uint8_t* dst) { 204 const size_t chunk_size = chunk->data_.size; 205 assert(chunk); 206 assert(chunk->tag_ != NIL_TAG); 207 PutLE32(dst + 0, chunk->tag_); 208 PutLE32(dst + TAG_SIZE, (uint32_t)chunk_size); 209 assert(chunk_size == (uint32_t)chunk_size); 210 memcpy(dst + CHUNK_HEADER_SIZE, chunk->data_.bytes, chunk_size); 211 if (chunk_size & 1) 212 dst[CHUNK_HEADER_SIZE + chunk_size] = 0; // Add padding. 213 return dst + ChunkDiskSize(chunk); 214 } 215 216 uint8_t* ChunkListEmit(const WebPChunk* chunk_list, uint8_t* dst) { 217 while (chunk_list != NULL) { 218 dst = ChunkEmit(chunk_list, dst); 219 chunk_list = chunk_list->next_; 220 } 221 return dst; 222 } 223 224 //------------------------------------------------------------------------------ 225 // Life of a MuxImage object. 226 227 void MuxImageInit(WebPMuxImage* const wpi) { 228 assert(wpi); 229 memset(wpi, 0, sizeof(*wpi)); 230 } 231 232 WebPMuxImage* MuxImageRelease(WebPMuxImage* const wpi) { 233 WebPMuxImage* next; 234 if (wpi == NULL) return NULL; 235 ChunkDelete(wpi->header_); 236 ChunkDelete(wpi->alpha_); 237 ChunkDelete(wpi->img_); 238 239 next = wpi->next_; 240 MuxImageInit(wpi); 241 return next; 242 } 243 244 //------------------------------------------------------------------------------ 245 // MuxImage search methods. 246 247 int MuxImageCount(const WebPMuxImage* wpi_list, WebPChunkId id) { 248 int count = 0; 249 const WebPMuxImage* current; 250 for (current = wpi_list; current != NULL; current = current->next_) { 251 if (id == WEBP_CHUNK_NIL) { 252 ++count; // Special case: count all images. 253 } else { 254 const WebPChunk* const wpi_chunk = *MuxImageGetListFromId(current, id); 255 if (wpi_chunk != NULL) { 256 const WebPChunkId wpi_chunk_id = ChunkGetIdFromTag(wpi_chunk->tag_); 257 if (wpi_chunk_id == id) ++count; // Count images with a matching 'id'. 258 } 259 } 260 } 261 return count; 262 } 263 264 // Outputs a pointer to 'prev_wpi->next_', 265 // where 'prev_wpi' is the pointer to the image at position (nth - 1). 266 // Returns true if nth image was found. 267 static int SearchImageToGetOrDelete(WebPMuxImage** wpi_list, uint32_t nth, 268 WebPMuxImage*** const location) { 269 uint32_t count = 0; 270 assert(wpi_list); 271 *location = wpi_list; 272 273 if (nth == 0) { 274 nth = MuxImageCount(*wpi_list, WEBP_CHUNK_NIL); 275 if (nth == 0) return 0; // Not found. 276 } 277 278 while (*wpi_list != NULL) { 279 WebPMuxImage* const cur_wpi = *wpi_list; 280 ++count; 281 if (count == nth) return 1; // Found. 282 wpi_list = &cur_wpi->next_; 283 *location = wpi_list; 284 } 285 return 0; // Not found. 286 } 287 288 //------------------------------------------------------------------------------ 289 // MuxImage writer methods. 290 291 WebPMuxError MuxImagePush(const WebPMuxImage* wpi, WebPMuxImage** wpi_list) { 292 WebPMuxImage* new_wpi; 293 294 while (*wpi_list != NULL) { 295 WebPMuxImage* const cur_wpi = *wpi_list; 296 if (cur_wpi->next_ == NULL) break; 297 wpi_list = &cur_wpi->next_; 298 } 299 300 new_wpi = (WebPMuxImage*)malloc(sizeof(*new_wpi)); 301 if (new_wpi == NULL) return WEBP_MUX_MEMORY_ERROR; 302 *new_wpi = *wpi; 303 new_wpi->next_ = NULL; 304 305 if (*wpi_list != NULL) { 306 (*wpi_list)->next_ = new_wpi; 307 } else { 308 *wpi_list = new_wpi; 309 } 310 return WEBP_MUX_OK; 311 } 312 313 //------------------------------------------------------------------------------ 314 // MuxImage deletion methods. 315 316 WebPMuxImage* MuxImageDelete(WebPMuxImage* const wpi) { 317 // Delete the components of wpi. If wpi is NULL this is a noop. 318 WebPMuxImage* const next = MuxImageRelease(wpi); 319 free(wpi); 320 return next; 321 } 322 323 void MuxImageDeleteAll(WebPMuxImage** const wpi_list) { 324 while (*wpi_list != NULL) { 325 *wpi_list = MuxImageDelete(*wpi_list); 326 } 327 } 328 329 WebPMuxError MuxImageDeleteNth(WebPMuxImage** wpi_list, uint32_t nth) { 330 assert(wpi_list); 331 if (!SearchImageToGetOrDelete(wpi_list, nth, &wpi_list)) { 332 return WEBP_MUX_NOT_FOUND; 333 } 334 *wpi_list = MuxImageDelete(*wpi_list); 335 return WEBP_MUX_OK; 336 } 337 338 //------------------------------------------------------------------------------ 339 // MuxImage reader methods. 340 341 WebPMuxError MuxImageGetNth(const WebPMuxImage** wpi_list, uint32_t nth, 342 WebPMuxImage** wpi) { 343 assert(wpi_list); 344 assert(wpi); 345 if (!SearchImageToGetOrDelete((WebPMuxImage**)wpi_list, nth, 346 (WebPMuxImage***)&wpi_list)) { 347 return WEBP_MUX_NOT_FOUND; 348 } 349 *wpi = (WebPMuxImage*)*wpi_list; 350 return WEBP_MUX_OK; 351 } 352 353 //------------------------------------------------------------------------------ 354 // MuxImage serialization methods. 355 356 // Size of an image. 357 size_t MuxImageDiskSize(const WebPMuxImage* const wpi) { 358 size_t size = 0; 359 if (wpi->header_ != NULL) size += ChunkDiskSize(wpi->header_); 360 if (wpi->alpha_ != NULL) size += ChunkDiskSize(wpi->alpha_); 361 if (wpi->img_ != NULL) size += ChunkDiskSize(wpi->img_); 362 return size; 363 } 364 365 size_t MuxImageListDiskSize(const WebPMuxImage* wpi_list) { 366 size_t size = 0; 367 while (wpi_list != NULL) { 368 size += MuxImageDiskSize(wpi_list); 369 wpi_list = wpi_list->next_; 370 } 371 return size; 372 } 373 374 // Special case as ANMF/FRGM chunk encapsulates other image chunks. 375 static uint8_t* ChunkEmitSpecial(const WebPChunk* const header, 376 size_t total_size, uint8_t* dst) { 377 const size_t header_size = header->data_.size; 378 const size_t offset_to_next = total_size - CHUNK_HEADER_SIZE; 379 assert(header->tag_ == kChunks[IDX_ANMF].tag || 380 header->tag_ == kChunks[IDX_FRGM].tag); 381 PutLE32(dst + 0, header->tag_); 382 PutLE32(dst + TAG_SIZE, (uint32_t)offset_to_next); 383 assert(header_size == (uint32_t)header_size); 384 memcpy(dst + CHUNK_HEADER_SIZE, header->data_.bytes, header_size); 385 if (header_size & 1) { 386 dst[CHUNK_HEADER_SIZE + header_size] = 0; // Add padding. 387 } 388 return dst + ChunkDiskSize(header); 389 } 390 391 uint8_t* MuxImageEmit(const WebPMuxImage* const wpi, uint8_t* dst) { 392 // Ordering of chunks to be emitted is strictly as follows: 393 // 1. ANMF/FRGM chunk (if present). 394 // 2. ALPH chunk (if present). 395 // 3. VP8/VP8L chunk. 396 assert(wpi); 397 if (wpi->header_ != NULL) { 398 dst = ChunkEmitSpecial(wpi->header_, MuxImageDiskSize(wpi), dst); 399 } 400 if (wpi->alpha_ != NULL) dst = ChunkEmit(wpi->alpha_, dst); 401 if (wpi->img_ != NULL) dst = ChunkEmit(wpi->img_, dst); 402 return dst; 403 } 404 405 uint8_t* MuxImageListEmit(const WebPMuxImage* wpi_list, uint8_t* dst) { 406 while (wpi_list != NULL) { 407 dst = MuxImageEmit(wpi_list, dst); 408 wpi_list = wpi_list->next_; 409 } 410 return dst; 411 } 412 413 //------------------------------------------------------------------------------ 414 // Helper methods for mux. 415 416 int MuxHasLosslessImages(const WebPMuxImage* images) { 417 while (images != NULL) { 418 assert(images->img_ != NULL); 419 if (images->img_->tag_ == kChunks[IDX_VP8L].tag) { 420 return 1; 421 } 422 images = images->next_; 423 } 424 return 0; 425 } 426 427 uint8_t* MuxEmitRiffHeader(uint8_t* const data, size_t size) { 428 PutLE32(data + 0, MKFOURCC('R', 'I', 'F', 'F')); 429 PutLE32(data + TAG_SIZE, (uint32_t)size - CHUNK_HEADER_SIZE); 430 assert(size == (uint32_t)size); 431 PutLE32(data + TAG_SIZE + CHUNK_SIZE_BYTES, MKFOURCC('W', 'E', 'B', 'P')); 432 return data + RIFF_HEADER_SIZE; 433 } 434 435 WebPChunk** MuxGetChunkListFromId(const WebPMux* mux, WebPChunkId id) { 436 assert(mux != NULL); 437 switch (id) { 438 case WEBP_CHUNK_VP8X: return (WebPChunk**)&mux->vp8x_; 439 case WEBP_CHUNK_ICCP: return (WebPChunk**)&mux->iccp_; 440 case WEBP_CHUNK_ANIM: return (WebPChunk**)&mux->anim_; 441 case WEBP_CHUNK_EXIF: return (WebPChunk**)&mux->exif_; 442 case WEBP_CHUNK_XMP: return (WebPChunk**)&mux->xmp_; 443 case WEBP_CHUNK_UNKNOWN: return (WebPChunk**)&mux->unknown_; 444 default: return NULL; 445 } 446 } 447 448 WebPMuxError MuxValidateForImage(const WebPMux* const mux) { 449 const int num_images = MuxImageCount(mux->images_, WEBP_CHUNK_IMAGE); 450 const int num_frames = MuxImageCount(mux->images_, WEBP_CHUNK_ANMF); 451 const int num_fragments = MuxImageCount(mux->images_, WEBP_CHUNK_FRGM); 452 453 if (num_images == 0) { 454 // No images in mux. 455 return WEBP_MUX_NOT_FOUND; 456 } else if (num_images == 1 && num_frames == 0 && num_fragments == 0) { 457 // Valid case (single image). 458 return WEBP_MUX_OK; 459 } else { 460 // Frame/Fragment case OR an invalid mux. 461 return WEBP_MUX_INVALID_ARGUMENT; 462 } 463 } 464 465 static int IsNotCompatible(int feature, int num_items) { 466 return (feature != 0) != (num_items > 0); 467 } 468 469 #define NO_FLAG 0 470 471 // Test basic constraints: 472 // retrieval, maximum number of chunks by index (use -1 to skip) 473 // and feature incompatibility (use NO_FLAG to skip). 474 // On success returns WEBP_MUX_OK and stores the chunk count in *num. 475 static WebPMuxError ValidateChunk(const WebPMux* const mux, CHUNK_INDEX idx, 476 WebPFeatureFlags feature, 477 WebPFeatureFlags vp8x_flags, 478 int max, int* num) { 479 const WebPMuxError err = 480 WebPMuxNumChunks(mux, kChunks[idx].id, num); 481 if (err != WEBP_MUX_OK) return err; 482 if (max > -1 && *num > max) return WEBP_MUX_INVALID_ARGUMENT; 483 if (feature != NO_FLAG && IsNotCompatible(vp8x_flags & feature, *num)) { 484 return WEBP_MUX_INVALID_ARGUMENT; 485 } 486 return WEBP_MUX_OK; 487 } 488 489 WebPMuxError MuxValidate(const WebPMux* const mux) { 490 int num_iccp; 491 int num_exif; 492 int num_xmp; 493 int num_anim; 494 int num_frames; 495 int num_fragments; 496 int num_vp8x; 497 int num_images; 498 int num_alpha; 499 uint32_t flags; 500 WebPMuxError err; 501 502 // Verify mux is not NULL. 503 if (mux == NULL) return WEBP_MUX_INVALID_ARGUMENT; 504 505 // Verify mux has at least one image. 506 if (mux->images_ == NULL) return WEBP_MUX_INVALID_ARGUMENT; 507 508 err = WebPMuxGetFeatures(mux, &flags); 509 if (err != WEBP_MUX_OK) return err; 510 511 // At most one color profile chunk. 512 err = ValidateChunk(mux, IDX_ICCP, ICCP_FLAG, flags, 1, &num_iccp); 513 if (err != WEBP_MUX_OK) return err; 514 515 // At most one EXIF metadata. 516 err = ValidateChunk(mux, IDX_EXIF, EXIF_FLAG, flags, 1, &num_exif); 517 if (err != WEBP_MUX_OK) return err; 518 519 // At most one XMP metadata. 520 err = ValidateChunk(mux, IDX_XMP, XMP_FLAG, flags, 1, &num_xmp); 521 if (err != WEBP_MUX_OK) return err; 522 523 // Animation: ANIMATION_FLAG, ANIM chunk and ANMF chunk(s) are consistent. 524 // At most one ANIM chunk. 525 err = ValidateChunk(mux, IDX_ANIM, NO_FLAG, flags, 1, &num_anim); 526 if (err != WEBP_MUX_OK) return err; 527 err = ValidateChunk(mux, IDX_ANMF, NO_FLAG, flags, -1, &num_frames); 528 if (err != WEBP_MUX_OK) return err; 529 530 { 531 const int has_animation = !!(flags & ANIMATION_FLAG); 532 if (has_animation && (num_anim == 0 || num_frames == 0)) { 533 return WEBP_MUX_INVALID_ARGUMENT; 534 } 535 if (!has_animation && (num_anim == 1 || num_frames > 0)) { 536 return WEBP_MUX_INVALID_ARGUMENT; 537 } 538 } 539 540 // Fragmentation: FRAGMENTS_FLAG and FRGM chunk(s) are consistent. 541 err = ValidateChunk(mux, IDX_FRGM, FRAGMENTS_FLAG, flags, -1, &num_fragments); 542 if (err != WEBP_MUX_OK) return err; 543 544 // Verify either VP8X chunk is present OR there is only one elem in 545 // mux->images_. 546 err = ValidateChunk(mux, IDX_VP8X, NO_FLAG, flags, 1, &num_vp8x); 547 if (err != WEBP_MUX_OK) return err; 548 err = ValidateChunk(mux, IDX_VP8, NO_FLAG, flags, -1, &num_images); 549 if (err != WEBP_MUX_OK) return err; 550 if (num_vp8x == 0 && num_images != 1) return WEBP_MUX_INVALID_ARGUMENT; 551 552 // ALPHA_FLAG & alpha chunk(s) are consistent. 553 if (MuxHasLosslessImages(mux->images_)) { 554 if (num_vp8x > 0) { 555 // Special case: we have a VP8X chunk as well as some lossless images. 556 if (!(flags & ALPHA_FLAG)) return WEBP_MUX_INVALID_ARGUMENT; 557 } 558 } else { 559 err = ValidateChunk(mux, IDX_ALPHA, ALPHA_FLAG, flags, -1, &num_alpha); 560 if (err != WEBP_MUX_OK) return err; 561 } 562 563 // num_fragments & num_images are consistent. 564 if (num_fragments > 0 && num_images != num_fragments) { 565 return WEBP_MUX_INVALID_ARGUMENT; 566 } 567 568 return WEBP_MUX_OK; 569 } 570 571 #undef NO_FLAG 572 573 //------------------------------------------------------------------------------ 574 575 #if defined(__cplusplus) || defined(c_plusplus) 576 } // extern "C" 577 #endif 578