├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.rst ├── fbuildroot.py ├── include └── rcbin.h ├── src ├── librcbin.c └── rcbin.c └── tst.c /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.6) 2 | project(rcbin) 3 | 4 | if ("${CMAKE_SYSTEM}" MATCHES "Linux") 5 | find_package(PkgConfig) 6 | pkg_search_module(LIBELF REQUIRED libelf) 7 | 8 | link_libraries(m) 9 | endif () 10 | 11 | include_directories(include) 12 | 13 | add_library(librcbin STATIC src/librcbin.c) 14 | set_target_properties(librcbin PROPERTIES OUTPUT_NAME rcbin) 15 | target_include_directories(librcbin PUBLIC include) 16 | 17 | add_executable(rcbin src/rcbin.c) 18 | target_include_directories(rcbin PUBLIC ${LIBELF_INCLUDE_DIRS}) 19 | target_link_libraries(rcbin PUBLIC ${LIBELF_LIBRARIES}) 20 | 21 | add_executable(tst tst.c) 22 | target_link_libraries(tst librcbin) 23 | 24 | install(TARGETS librcbin rcbin 25 | RUNTIME DESTINATION bin 26 | ARCHIVE DESTINATION lib) 27 | install(FILES include/rcbin.h DESTINATION include) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ryan Gonzalez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | rcbin 2 | ===== 3 | 4 | rcbin lets you embed arbitrary resource data into your executables *after* they've already 5 | been compiled. 6 | 7 | Ever wanted to change your version number after compiling your binary, or allow someone 8 | else to embed something into it after it's been built? Trying to embed a scripting language 9 | into native binaries, except you need to embed the script *after* normal compilation? 10 | 11 | **rcbin may be what you're looking for!** 12 | 13 | Build instructions 14 | ****************** 15 | 16 | Due to some complications regarding recent Fbuild changes, CMake is temporarily replacing 17 | Fbuild for building. Just build it like you would any other CMake project:: 18 | 19 | $ mkdir build 20 | $ cd build 21 | $ cmake .. -G Ninja 22 | $ ninja 23 | 24 | or Windows/MSVC equivalent. 25 | 26 | Usage 27 | ***** 28 | 29 | Here's an example: 30 | 31 | .. code-block:: c 32 | 33 | #include 34 | 35 | // RCBIN_IMPORT is used to define a resource that rcbin will later include. This will define a 36 | // function myresource() that can be used to load the resource. 37 | RCBIN_IMPORT(myresource) 38 | 39 | int main() { 40 | if (!rcbin_init()) { 41 | puts("Failed to initialize rcbin"); 42 | return 1; 43 | } 44 | 45 | // Load our resource by calling myresource(). 46 | rcbin_entry res = myresource(); 47 | // If res.data != NULL, then the resource was loaded successfully. The data is in res.data, 48 | // and the size in res.sz. 49 | if (res.data != NULL) { 50 | printf("myresource: %.*s\n", (int)res.sz, res.data); 51 | return 0; 52 | } else { 53 | // Otherwise, fail. 54 | puts("Failed to get myresource!"); 55 | return 1; 56 | } 57 | } 58 | 59 | Now, after building your program, run rcbin on it:: 60 | 61 | $ rcbin my_program myresource =message 62 | 63 | *rcbin* takes the program to modify and an even number of arguments. First comes the 64 | resource name, then its contents preceded by an equals sign. This command will define 65 | *myresource* to be equal to the string *message*. 66 | 67 | If you want to read the contents of a file into a resource instead, use ``@``:: 68 | 69 | $ rcbin my_program myresource @my_file 70 | 71 | This will define *myresource* to be equal to the contents of *my_file*. 72 | 73 | Need help? 74 | ********** 75 | 76 | Open up an issue, or `ping me on Twitter `_. 77 | -------------------------------------------------------------------------------- /fbuildroot.py: -------------------------------------------------------------------------------- 1 | from fbuild.builders.platform import guess_platform 2 | from fbuild.builders.c import guess 3 | 4 | from fbuild.config.c import cacheproperty, header_test, Test 5 | 6 | from fbuild.record import Record 7 | import fbuild.db 8 | 9 | from optparse import make_option 10 | import ast 11 | 12 | 13 | def arguments(parser): 14 | group = parser.add_argument_group('config options') 15 | group.add_argument('--cc', help='Use the given C compiler'), 16 | group.add_argument('--cflag', help='Pass the given flag to the C compiler', 17 | action='append', default=[]), 18 | group.add_argument('--use-color', help='Force C++ compiler colored output', 19 | action='store_true', default=True), 20 | group.add_argument('--release', help='Build in release mode', 21 | action='store_true', default=False), 22 | group.add_argument('--platform', help='Use the given target platform for building'), 23 | 24 | 25 | class libelf(Test): 26 | libelf_h = header_test('libelf.h') 27 | gelf_h = header_test('gelf.h') 28 | 29 | 30 | @fbuild.db.caches 31 | def configure(ctx): 32 | if ctx.options.platform: 33 | platform = ast.literal_eval(ctx.options.platform) 34 | else: 35 | platform = guess_platform(ctx) 36 | 37 | flags = ctx.options.cflag 38 | posix_flags = ['-Wall', '-Werror'] 39 | clang_flags = [] 40 | 41 | if ctx.options.use_color: 42 | posix_flags.append('-fdiagnostics-color') 43 | if ctx.options.release: 44 | debug = False 45 | optimize = True 46 | else: 47 | debug = True 48 | optimize = False 49 | clang_flags.append('-fno-limit-debug-info') 50 | 51 | c = guess.static(ctx, platform=platform, exe=ctx.options.cc, flags=flags, 52 | debug=debug, optimize=optimize, platform_options=[ 53 | ({'clang'}, {'flags+': clang_flags}), 54 | ({'posix'}, {'flags+': posix_flags, 55 | 'external_libs+': ['m'], 56 | 'cross_compiler': True}), 57 | ]) 58 | 59 | external_libs = [] 60 | 61 | if platform & {'posix'} and not platform & {'mingw'}: 62 | libelf_test = libelf(c) 63 | if not libelf_test.libelf_h or not libelf_test.gelf_h: 64 | raise fbuild.ConfigFailed('libelf is required') 65 | external_libs.append('elf') 66 | 67 | return Record(c=c, external_libs=external_libs) 68 | 69 | 70 | def build(ctx): 71 | rec = configure(ctx) 72 | librcbin = rec.c.build_lib('rcbin', ['src/librcbin.c'], includes=['include']) 73 | rcbin = rec.c.build_exe('rcbin', ['src/rcbin.c'], includes=['include'], 74 | external_libs=rec.external_libs) 75 | rec.c.build_exe('tst', ['tst.c'], includes=['include'], libs=[librcbin]) 76 | 77 | ctx.install('include/rcbin.h', 'include') 78 | ctx.install(librcbin, 'lib') 79 | ctx.install(rcbin, 'bin') 80 | -------------------------------------------------------------------------------- /include/rcbin.h: -------------------------------------------------------------------------------- 1 | #ifndef RCBIN_H 2 | #define RCBIN_H 3 | 4 | 5 | #include 6 | #include 7 | 8 | 9 | #ifdef RCBIN_NO_THREAD_LOCAL 10 | #define RCBIN_THREAD_LOCAL 11 | #elif _MSC_VER 12 | #define RCBIN_THREAD_LOCAL __declspec(thread) 13 | #elif __GNUC__ 14 | #define RCBIN_THREAD_LOCAL __thread 15 | #else 16 | #error Unsupported platform/compiler for thread local. \ 17 | Try passing -DRCBIN_NO_THREAD_LOCAL. 18 | #endif 19 | 20 | #ifdef _WIN32 21 | 22 | #define RCBIN_PLATFORM_PREFIX "rcbin_res_" 23 | 24 | #else // !_WIN32 25 | 26 | #include 27 | 28 | #define RCBIN_PLATFORM_PREFIX "" 29 | #define RCBIN_MAGIC 0x3CB1 30 | 31 | 32 | #ifdef RCBIN_INTERNAL 33 | 34 | #define RCBIN_SECTION "__rcbin_internal_root_section" 35 | 36 | typedef struct rcbin_root rcbin_root; 37 | typedef struct rcbin_entry_header rcbin_entry_header; 38 | 39 | struct rcbin_root { 40 | uint32_t magic; 41 | uint64_t offs; 42 | }; 43 | 44 | struct rcbin_entry_header { 45 | uint64_t name_sz, data_sz; 46 | }; 47 | 48 | 49 | 50 | #endif // RCBIN_INTERNAL 51 | 52 | #endif // _WIN32 53 | 54 | 55 | typedef struct rcbin_entry rcbin_entry; 56 | 57 | struct rcbin_entry { 58 | size_t sz; 59 | const void* data; 60 | }; 61 | 62 | 63 | int rcbin_init(); 64 | void* rcbin_alloc(size_t sz); 65 | void rcbin_lookup(const char* name, const void** data_out, size_t* sz_out); 66 | 67 | 68 | #define RCBIN_IMPORT(name) \ 69 | rcbin_entry name() { \ 70 | static RCBIN_THREAD_LOCAL size_t rcbin_sz_cache=-1; \ 71 | static RCBIN_THREAD_LOCAL const void* rcbin_data_cache=NULL; \ 72 | if (rcbin_data_cache == NULL) \ 73 | rcbin_lookup(RCBIN_PLATFORM_PREFIX #name, &rcbin_data_cache, \ 74 | &rcbin_sz_cache); \ 75 | rcbin_entry e; e.sz = rcbin_sz_cache; e.data = rcbin_data_cache; \ 76 | return e; } 77 | 78 | #endif // RCBIN_H 79 | -------------------------------------------------------------------------------- /src/librcbin.c: -------------------------------------------------------------------------------- 1 | #define RCBIN_INTERNAL 2 | #include 3 | 4 | 5 | #ifdef _WIN32 6 | 7 | #include 8 | 9 | 10 | int rcbin_init() { return 1; } 11 | 12 | 13 | void rcbin_lookup(const char* name, const void** data_out, size_t* sz_out) { 14 | HRSRC resinfo = FindResource(NULL, name, RT_RCDATA); 15 | if (resinfo == NULL) return; 16 | 17 | size_t sz = SizeofResource(NULL, resinfo); 18 | if (sz == 0) return; 19 | 20 | HGLOBAL resdata = LoadResource(NULL, resinfo); 21 | if (resdata == NULL) return; 22 | 23 | LPVOID res = LockResource(resdata); 24 | if (res == NULL) return; 25 | 26 | *data_out = res; 27 | *sz_out = sz; 28 | } 29 | 30 | 31 | #else 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | 39 | // This will be modified 40 | rcbin_root __rcbin_internal_root 41 | __attribute__((__section__(RCBIN_SECTION))) = {0}; 42 | // Contains the rcbin entries from the executable file. 43 | static void* entries = NULL; 44 | 45 | 46 | static void read_executable_path(const char* path) { 47 | int fd = open(path, O_RDONLY); 48 | if (fd == -1) return; 49 | 50 | if (lseek(fd, __rcbin_internal_root.offs, SEEK_SET) == -1) return; 51 | 52 | size_t total_sz = lseek(fd, 0, SEEK_END); 53 | if (total_sz == -1) return; 54 | 55 | if (total_sz < __rcbin_internal_root.offs) return; 56 | if (lseek(fd, __rcbin_internal_root.offs, SEEK_SET) == -1) return; 57 | 58 | size_t sz = total_sz - __rcbin_internal_root.offs; 59 | 60 | void* buf = malloc(sz); 61 | if (read(fd, buf, sz) == -1) { 62 | free(buf); 63 | return; 64 | } 65 | 66 | entries = buf; 67 | } 68 | 69 | 70 | // Platform-specific executable finding... 71 | 72 | #ifdef __APPLE__ 73 | 74 | #include 75 | 76 | static void read_executable() { 77 | char path[PATH_MAX]; 78 | size_t sz; 79 | if (_NSGetExecutablePath(path, &sz)) return; 80 | read_executable_path(path); 81 | } 82 | 83 | #elif __sun 84 | 85 | static void read_executable() { 86 | char* path = getexecname(); 87 | if (path) read_executable_path(path); 88 | } 89 | 90 | #elif __FreeBSD__ 91 | 92 | #include 93 | 94 | static void read_executable() { 95 | int name[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; 96 | char path[PATH_MAX]; 97 | size_t sz; 98 | if (sysctl(name, 4, path, &sz, NULL, 0) == 0) { 99 | read_executable_path(path); 100 | } 101 | } 102 | 103 | #elif __NetBSD__ 104 | 105 | static void read_executable() { 106 | read_executable_path("/proc/curproc/exe"); 107 | } 108 | 109 | #elif __DragonFly__ 110 | 111 | static void read_executable() { 112 | read_executable_path("/proc/curproc/file"); 113 | } 114 | 115 | #elif __linux__ 116 | 117 | static void read_executable() { 118 | read_executable_path("/proc/self/exe"); 119 | } 120 | 121 | #else 122 | 123 | #error Unsupported platform. 124 | 125 | #endif 126 | 127 | 128 | int rcbin_init() { 129 | if (__rcbin_internal_root.magic != RCBIN_MAGIC) return 0; 130 | 131 | read_executable(); 132 | return entries != NULL; 133 | } 134 | 135 | 136 | void rcbin_lookup(const char* name, const void** data_out, size_t* sz_out) { 137 | const void* pos = entries; 138 | size_t name_sz = strlen(name); 139 | 140 | for (;;) { 141 | // Grab the header and check the name. If it's the one we're looking for, then 142 | // return the data. 143 | rcbin_entry_header* eh = (rcbin_entry_header*)pos; 144 | if (eh->name_sz == 0) break; // name_sz == 0 signifies the end 145 | 146 | const char* e_name_begin = pos + sizeof(rcbin_entry_header); 147 | if (eh->name_sz == name_sz && memcmp(e_name_begin, name, name_sz) == 0) { 148 | *data_out = e_name_begin + eh->name_sz; 149 | *sz_out = eh->data_sz; 150 | return; 151 | } 152 | 153 | pos += sizeof(rcbin_entry_header) + eh->name_sz + eh->data_sz; 154 | } 155 | } 156 | 157 | #endif 158 | -------------------------------------------------------------------------------- /src/rcbin.c: -------------------------------------------------------------------------------- 1 | #define RCBIN_INTERNAL 2 | #include 3 | 4 | #include 5 | 6 | 7 | typedef struct rcbin_context rcbin_context; 8 | 9 | 10 | #ifdef _WIN32 11 | 12 | #include 13 | 14 | 15 | struct rcbin_context { 16 | HANDLE h; 17 | }; 18 | 19 | 20 | int ctx_init(rcbin_context* ctx, const char* path) { 21 | ctx->h = BeginUpdateResource(path, FALSE); 22 | return ctx->h != NULL; 23 | } 24 | 25 | 26 | int ctx_add(rcbin_context* ctx, const char* name, void* data, size_t data_sz) { 27 | size_t name_sz = strlen(name); 28 | size_t prefix_sz = strlen(RCBIN_PLATFORM_PREFIX); 29 | 30 | char* prefixed_name = malloc(prefix_sz+name_sz+1); 31 | memcpy(prefixed_name, RCBIN_PLATFORM_PREFIX, prefix_sz); 32 | memcpy(prefixed_name+prefix_sz, name, name_sz+1); 33 | 34 | BOOL status = UpdateResource(ctx->h, RT_RCDATA, prefixed_name, 35 | MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), data, 36 | data_sz); 37 | 38 | free(prefixed_name); 39 | return status == TRUE; 40 | } 41 | 42 | 43 | int ctx_save(rcbin_context* ctx) { 44 | return EndUpdateResource(ctx->h, FALSE) == TRUE; 45 | } 46 | 47 | 48 | #else 49 | 50 | #include 51 | #include 52 | 53 | #include 54 | #include 55 | #include 56 | 57 | 58 | #define NEXT_POWER_OF_2(x) ((x) == 0 ? 2 : pow(2, ceil(log2(x)))) 59 | 60 | 61 | struct rcbin_context { 62 | int fd; 63 | Elf* e; 64 | size_t root_in_file_offs, data_in_file_offs; 65 | rcbin_root root; 66 | 67 | void* data; 68 | size_t data_sz, data_offs; 69 | }; 70 | 71 | 72 | int ctx_init(rcbin_context* ctx, const char* path) { 73 | if (elf_version(EV_CURRENT) == EV_NONE) return 0; 74 | 75 | ctx->fd = open(path, O_RDWR); 76 | if (ctx->fd == -1) return 0; 77 | 78 | size_t file_sz = lseek(ctx->fd, 0, SEEK_END); 79 | if (file_sz == -1) return 0; 80 | ctx->data_in_file_offs = file_sz; 81 | 82 | lseek(ctx->fd, 0, SEEK_SET); 83 | 84 | ctx->e = elf_begin(ctx->fd, ELF_C_READ, NULL); 85 | if (ctx->e == NULL) return 0; 86 | if (elf_kind(ctx->e) != ELF_K_ELF) return 0; 87 | 88 | size_t shstrndx; 89 | elf_getshdrstrndx(ctx->e, &shstrndx); 90 | 91 | ctx->root.magic = 0; 92 | 93 | // Find the rcbin entries section. 94 | Elf_Scn* scn = NULL; 95 | while ((scn = elf_nextscn(ctx->e, scn))) { 96 | GElf_Shdr shdr; 97 | gelf_getshdr(scn, &shdr); 98 | 99 | if (strcmp(elf_strptr(ctx->e, shstrndx, shdr.sh_name), RCBIN_SECTION) == 0) { 100 | // Grab the root info. 101 | Elf_Data* root_data = elf_getdata(scn, NULL); 102 | 103 | rcbin_root* prev_r = root_data->d_buf; 104 | if (prev_r->magic == RCBIN_MAGIC) { 105 | // rcbin has already been run once; keep the file offsets identical. 106 | ctx->data_in_file_offs = prev_r->offs; 107 | } 108 | 109 | // Save the root header. 110 | ctx->root.magic = RCBIN_MAGIC; 111 | ctx->root.offs = ctx->data_in_file_offs; 112 | ctx->root_in_file_offs = shdr.sh_offset + root_data->d_off; 113 | } 114 | } 115 | 116 | ctx->data = malloc(2); 117 | ctx->data_sz = 2; 118 | ctx->data_offs = 0; 119 | 120 | return ctx->root.magic != 0; 121 | } 122 | 123 | 124 | int ctx_grow(rcbin_context* ctx, int expand) { 125 | ctx->data_offs += expand; 126 | 127 | // If growing is necessary, grow up till the next power of two. 128 | if (ctx->data_offs > ctx->data_sz) { 129 | ctx->data_sz = NEXT_POWER_OF_2(ctx->data_offs); 130 | ctx->data = realloc(ctx->data, ctx->data_sz); 131 | if (ctx->data == NULL) return 0; 132 | } 133 | 134 | return 1; 135 | } 136 | 137 | 138 | int ctx_add(rcbin_context* ctx, const char* name, void* data, size_t data_sz) { 139 | size_t current_offset = ctx->data_offs; 140 | size_t name_sz = strlen(name); 141 | if (!ctx_grow(ctx, sizeof(rcbin_entry_header) + name_sz + data_sz)) return 0; 142 | 143 | // Setup the entry header. 144 | struct rcbin_entry_header eh; 145 | 146 | eh.name_sz = name_sz; 147 | eh.data_sz = data_sz; 148 | 149 | // Write the header and data. 150 | memcpy(ctx->data+current_offset, &eh, sizeof(eh)); 151 | current_offset += sizeof(eh); 152 | memcpy(ctx->data+current_offset, name, name_sz); 153 | current_offset += name_sz; 154 | memcpy(ctx->data+current_offset, data, data_sz); 155 | current_offset += data_sz; 156 | 157 | return 1; 158 | } 159 | 160 | 161 | int ctx_save(rcbin_context* ctx) { 162 | size_t current_offset = ctx->data_offs; 163 | if (!ctx_grow(ctx, sizeof(uint64_t))) return 0; 164 | 165 | // a name_sz of 0 signifies the end 166 | uint64_t tail = 0; 167 | memcpy(ctx->data+current_offset, &tail, sizeof(uint64_t)); 168 | 169 | if (lseek(ctx->fd, ctx->root_in_file_offs, SEEK_SET) == -1) return 0; 170 | if (write(ctx->fd, &ctx->root, sizeof(rcbin_root)) == -1) return 0; 171 | 172 | if (lseek(ctx->fd, ctx->data_in_file_offs, SEEK_SET) == -1) return 0; 173 | if (write(ctx->fd, ctx->data, ctx->data_offs) == -1) return 0; 174 | 175 | fsync(ctx->fd); 176 | close(ctx->fd); 177 | 178 | free(ctx->data); 179 | return 1; 180 | } 181 | 182 | 183 | #endif 184 | 185 | 186 | void usage(const char* self) { 187 | printf("usage: %s executable-file [resource-name resource-value...]\n", self); 188 | puts("resource-value should be =value for string value, @value for file contents."); 189 | printf("example: %s myapp myresource @myfile\n", self); 190 | printf("example: %s myapp myresource =mystring\n", self); 191 | } 192 | 193 | 194 | int read_file_data(const char* path, void** data_out, size_t* sz_out) { 195 | FILE* fp = fopen(path, "rb"); 196 | if (fp == NULL) return 0; 197 | 198 | if (fseek(fp, 0, SEEK_END) != 0) return 0; 199 | *sz_out = ftell(fp); 200 | fseek(fp, 0, SEEK_SET); 201 | 202 | *data_out = malloc(*sz_out); 203 | if (fread(*data_out, 1, *sz_out, fp) == -1) return 0; 204 | 205 | fclose(fp); 206 | return 1; 207 | } 208 | 209 | 210 | #define ISEVEN(n) ((n) % 2 == 0) 211 | 212 | 213 | int main(int argc, char** argv) { 214 | if (argc < 2 || !ISEVEN(argc)) { 215 | usage(argv[0]); 216 | return 1; 217 | } 218 | 219 | if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "-?") == 0 || 220 | strcmp(argv[1], "--help") == 0) { 221 | usage(argv[0]); 222 | return 0; 223 | } 224 | 225 | rcbin_context ctx; 226 | if (!ctx_init(&ctx, argv[1])) { 227 | puts("failed to read file; are you sure it exists and is a valid ELF file?"); 228 | return 1; 229 | } 230 | 231 | for (int i=2; i 2 | 3 | #include 4 | #include 5 | 6 | 7 | RCBIN_IMPORT(abc) 8 | 9 | 10 | int main() { 11 | if (!rcbin_init()) { 12 | puts("Failed to initialize rcbin."); 13 | return 1; 14 | } 15 | 16 | rcbin_entry message_data = abc(); 17 | if (message_data.data == NULL) { 18 | puts("Failed to retrieve message."); 19 | } else { 20 | printf("%.*s\n", (int)message_data.sz, message_data.data); 21 | } 22 | 23 | return 0; 24 | } 25 | --------------------------------------------------------------------------------