├── .gitignore ├── c_src ├── .gitignore └── io_libc.c ├── ebin └── .gitignore ├── priv └── .gitignore ├── rebar ├── rebar.config ├── src ├── io_libc.app.src ├── io_libc_app.erl └── io_libc.erl └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | -------------------------------------------------------------------------------- /c_src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | -------------------------------------------------------------------------------- /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /priv/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxlapshin/io_libc/HEAD/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {port_specs, [ 2 | {"priv/io_libc.so", ["c_src/*.c"]} 3 | ]}. 4 | {port_env, [ 5 | {".*", "CFLAGS", "$CFLAGS -Wall -O2"} 6 | ]}. 7 | 8 | -------------------------------------------------------------------------------- /src/io_libc.app.src: -------------------------------------------------------------------------------- 1 | {application, io_libc, [ 2 | {description, "Fast libc based formatter with io_lib-like api"}, 3 | {vsn, "0.1"}, 4 | {registered, []}, 5 | {applications, [kernel, stdlib]}, 6 | {mod, {io_libc_app, []}} 7 | ]}. 8 | 9 | -------------------------------------------------------------------------------- /src/io_libc_app.erl: -------------------------------------------------------------------------------- 1 | -module(io_libc_app). 2 | -export([start/2, stop/1]). 3 | -export([init/1]). 4 | 5 | start(_, _) -> 6 | % Ensure module loads and start fake supervisor on success 7 | case code:load_file(io_libc) of 8 | {module, io_libc} -> 9 | supervisor:start_link({local, io_libc_sup}, ?MODULE, []); 10 | Other -> 11 | Other 12 | end. 13 | 14 | stop(_) -> 15 | erlang:exit(erlang:whereis(?MODULE), shutdown). 16 | 17 | init([]) -> 18 | {ok, {{one_for_one, 10, 10}, []}}. 19 | -------------------------------------------------------------------------------- /src/io_libc.erl: -------------------------------------------------------------------------------- 1 | -module(io_libc). 2 | 3 | -export([fwrite/2, format/2]). 4 | -export([utc_to_datetime/1]). 5 | -export([init_nif/0]). 6 | 7 | -on_load(init_nif/0). 8 | 9 | init_nif() -> 10 | NifDir = case code:priv_dir(io_libc) of 11 | {error, _} -> 12 | SelfPath = code:which(?MODULE), 13 | EbinDir = filename:dirname(SelfPath), 14 | PacketDir = filename:dirname(EbinDir), 15 | filename:join(PacketDir, priv); 16 | PrivDir -> 17 | PrivDir 18 | end, 19 | 20 | NifPath = filename:join(NifDir, io_libc), 21 | erlang:load_nif(NifPath, 0). 22 | 23 | 24 | format(Format, Data) -> 25 | fwrite(Format, Data). 26 | 27 | fwrite(_Format, _Data) -> 28 | erlang:nif_error({io_libc, nif_not_loaded}). 29 | 30 | 31 | utc_to_datetime(_) -> 32 | erlang:nif_error({io_libc, nif_not_loaded}). 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `io_libc` -- fast formatter NIF for Erlang using standard C library 2 | =================== 3 | 4 | API 5 | --------- 6 | Currently, the only working function is `io_libc:fwrite/2`, `io_libc:format/2` is alias for it. 7 | 8 | Usage 9 | ------------- 10 | To format strings faster, call `io_libc:format` instead of `io_lib:format`, providing C-style fmt string. 11 | Example: 12 | 13 | > io_libc:format("hello, %08.3f, %s %04X!", [3.14, dead, 48879]). 14 | <<"hello, 0003.140, dead BEEF!">> 15 | 16 | Numbers are converted automatically when needed, falling back to zero on error: 17 | 18 | > io_libc:format("float %.1f, int %d, bad %d and %f", [3, 2.7, {eee}]). 19 | <<"float 3.0, int 2, bad 0 and 0.000000">> 20 | 21 | You can pass atom or any valid iolist as string value: 22 | 23 | > io_libc:format("string %s, atom %s, bad '%s' and '%s'", [["abc", [<<"de">>], $f], hello, {foo}]). 24 | <<"string abcdef, atom hello, bad '' and ''">> 25 | 26 | Extra convertion arguments are also supported: 27 | 28 | > io_libc:format("%*.*f; %0*d", [8, 3, 2.7, 6, 12]). 29 | <<" 2.700; 000012">> 30 | -------------------------------------------------------------------------------- /c_src/io_libc.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include "erl_nif.h" 6 | 7 | #define ARG_NONE 0 8 | #define ARG_INT 1 9 | #define ARG_LONG 2 10 | #define ARG_UINT 3 11 | #define ARG_ULONG 4 12 | #define ARG_DOUBLE 6 13 | #define ARG_STRING 7 14 | 15 | // Fetchers for every argument type 16 | int fetch_int(ErlNifEnv*, ERL_NIF_TERM*); 17 | long fetch_long(ErlNifEnv*, ERL_NIF_TERM*); 18 | unsigned int fetch_uint(ErlNifEnv*, ERL_NIF_TERM*); 19 | unsigned long fetch_ulong(ErlNifEnv*, ERL_NIF_TERM*); 20 | double fetch_double(ErlNifEnv*, ERL_NIF_TERM*); 21 | char* fetch_string(ErlNifEnv*, ERL_NIF_TERM*); 22 | 23 | 24 | static ERL_NIF_TERM 25 | utc_to_datetime(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { 26 | if(argc != 1) { 27 | return enif_make_badarg(env); 28 | } 29 | unsigned int utc; 30 | if(!enif_get_uint(env, argv[0], &utc)) { 31 | return enif_make_badarg(env); 32 | } 33 | 34 | time_t now = utc; 35 | struct tm result; 36 | gmtime_r(&now, &result); 37 | return enif_make_tuple2(env, 38 | enif_make_tuple3(env, 39 | enif_make_int(env, result.tm_year + 1900), 40 | enif_make_int(env, result.tm_mon + 1), 41 | enif_make_int(env, result.tm_mday) 42 | ), 43 | enif_make_tuple3(env, 44 | enif_make_int(env, result.tm_hour), 45 | enif_make_int(env, result.tm_min), 46 | enif_make_int(env, result.tm_sec) 47 | ) 48 | ); 49 | } 50 | 51 | 52 | // Iterable function which gets first convertion spec from input and appends formatted data to given binary 53 | int format_first(ErlNifEnv*, ErlNifBinary*, char**, ERL_NIF_TERM*); 54 | 55 | // fwrite implementation 56 | static ERL_NIF_TERM 57 | io_libc_fwrite(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { 58 | char fmt[1024]; 59 | int len = 0; 60 | 61 | if((len = enif_get_string(env, argv[0], fmt, sizeof(fmt), ERL_NIF_LATIN1)) <= 0) return enif_make_badarg(env); 62 | if(!enif_is_list(env, argv[1])) return enif_make_badarg(env); 63 | 64 | ERL_NIF_TERM items = argv[1]; 65 | 66 | // At first, allocate result as empty binary 67 | ErlNifBinary result; 68 | enif_alloc_binary(0, &result); 69 | 70 | // Get escape sequences one-by-one from fmt with according values from items 71 | char *cur_fmt = fmt; 72 | int format_result; 73 | while ((format_result = format_first(env, &result, &cur_fmt, &items)) > 0) { 74 | // Nothing to do here, everything is ok 75 | } 76 | 77 | // good: return resulting binary 78 | if (format_result >= 0) return enif_make_binary(env, &result); 79 | 80 | // bad: make badarg. TODO: return something pointing to erroneous place 81 | return enif_make_badarg(env); 82 | }; 83 | 84 | 85 | // Helper marcos for formatting numeric types 86 | #define fetch_asprintf(fetcher) \ 87 | switch (consumed) { \ 88 | case 1: \ 89 | asprintf(&formatted, fmt_start, fetcher(env, items)); \ 90 | break; \ 91 | case 2: \ 92 | e1 = fetch_int(env, items); \ 93 | asprintf(&formatted, fmt_start, e1, fetcher(env, items)); \ 94 | break; \ 95 | case 3: \ 96 | e1 = fetch_int(env, items); \ 97 | e2 = fetch_int(env, items); \ 98 | asprintf(&formatted, fmt_start, e1, e2, fetcher(env, items)); \ 99 | break; \ 100 | }; \ 101 | break; 102 | 103 | int format_first(ErlNifEnv* env, ErlNifBinary* result, char** fmt_start_ptr, ERL_NIF_TERM* items) { 104 | char* fmt_start = *fmt_start_ptr; 105 | char* fmt_iter = fmt_start; 106 | int consumed = 0; 107 | int fmt_long = 0; 108 | int arg_type = ARG_NONE; 109 | 110 | // started at the end of line 111 | if (*fmt_iter == 0) return 0; 112 | 113 | // find first covnertion spec character or string end 114 | while (*fmt_iter != 0) { 115 | if (*fmt_iter == '%') { 116 | // Ignore "%%" as it does not take any arguments 117 | if (*(fmt_iter+1) == '%') 118 | fmt_iter += 2; 119 | else { 120 | arg_type = -1; 121 | break; 122 | }; 123 | } 124 | else fmt_iter += 1; 125 | } 126 | 127 | // find convertion specifier 128 | while (*fmt_iter != 0){ 129 | if (index("dic", *fmt_iter) != NULL) { 130 | arg_type = fmt_long?ARG_LONG:ARG_INT; 131 | consumed++; 132 | fmt_iter++; 133 | break; 134 | }; 135 | if (index("ouxX", *fmt_iter) != NULL) { 136 | arg_type = fmt_long?ARG_ULONG:ARG_UINT; 137 | consumed++; 138 | fmt_iter++; 139 | break; 140 | }; 141 | if (index("eEfFgGaA", *fmt_iter) != NULL) { 142 | arg_type = ARG_DOUBLE; 143 | consumed++; 144 | fmt_iter++; 145 | break; 146 | }; 147 | if (*fmt_iter == 's') { 148 | arg_type = ARG_STRING; 149 | consumed++; 150 | fmt_iter++; 151 | break; 152 | }; 153 | 154 | // Extra argument is consumed from input 155 | if (*fmt_iter == '*') consumed++; 156 | 157 | // We don't support references to argument positions -> return error 158 | if (*fmt_iter == '$') return -1; 159 | 160 | // Support long 161 | if (*fmt_iter == 'l') fmt_long = 1; 162 | 163 | // At last, go to next character 164 | fmt_iter++; 165 | }; 166 | 167 | // convertion spec opened but not closed 168 | if (arg_type < 0) return -1; 169 | // Too much stars in spec 170 | if (consumed > 3) return -1; 171 | 172 | // To not copy part of format, just set zero where scan has ended, then return original value back 173 | char kept_fmt_byte = *fmt_iter; 174 | *fmt_iter = 0; 175 | 176 | // We don't really know how much memory will be used, so we call asprintf to let it allocate as much as it needs 177 | // Do not forget to call free() in the end! 178 | char* formatted = NULL; 179 | char* stringbuf; 180 | 181 | int e1, e2; 182 | 183 | switch (arg_type) { 184 | case ARG_NONE: 185 | asprintf(&formatted, fmt_start); 186 | break; 187 | // Numeric types: see macros above 188 | case ARG_INT: fetch_asprintf(fetch_int); 189 | case ARG_LONG: fetch_asprintf(fetch_long); 190 | case ARG_UINT: fetch_asprintf(fetch_uint); 191 | case ARG_ULONG: fetch_asprintf(fetch_ulong); 192 | case ARG_DOUBLE: fetch_asprintf(fetch_double); 193 | 194 | case ARG_STRING: // This cannot be handled by that simple macros because of allocation inside fetch_string 195 | switch (consumed) { 196 | case 1: 197 | asprintf(&formatted, fmt_start, stringbuf = fetch_string(env, items)); 198 | free(stringbuf); 199 | break; 200 | case 2: 201 | e1 = fetch_int(env, items); 202 | asprintf(&formatted, fmt_start, e1, stringbuf = fetch_string(env, items)); 203 | free(stringbuf); 204 | break; 205 | case 3: 206 | e1 = fetch_int(env, items); 207 | e2 = fetch_int(env, items); 208 | asprintf(&formatted, fmt_start, e1, e2, stringbuf = fetch_string(env, items)); 209 | free(stringbuf); 210 | break; 211 | }; 212 | break; 213 | }; 214 | 215 | // Put back saved byte and move format pointer 216 | *fmt_iter = kept_fmt_byte; 217 | *fmt_start_ptr = fmt_iter; 218 | 219 | // asprintf did not run or returned error 220 | if (formatted == NULL || formatted < 0) return -2; 221 | 222 | // remember where we start copying formatted part 223 | int oldsize = result->size; 224 | 225 | // Here we are sure formatted contains zero-terminated string, so strlen is safe 226 | int part_length = strlen(formatted); 227 | enif_realloc_binary(result, oldsize + part_length); 228 | 229 | // Do copy from formatted to result buffer 230 | char* copy_dest = (char*)result->data + oldsize; 231 | memcpy(copy_dest, formatted, part_length); 232 | 233 | // free memory allocated by asprintf 234 | free(formatted); 235 | 236 | // Return number of elements read from items 237 | return consumed; 238 | }; 239 | 240 | 241 | int 242 | fetch_int(ErlNifEnv* env, ERL_NIF_TERM* items) { 243 | ERL_NIF_TERM head; 244 | // Fetch head if possible 245 | if (! enif_get_list_cell(env, *items, &head, items)) return 0; 246 | 247 | int i; 248 | double d; 249 | 250 | if(enif_get_int(env, head, &i)) return i; 251 | if(enif_get_double(env, head, &d)) return (int) d; 252 | 253 | return 0; 254 | }; 255 | 256 | unsigned int 257 | fetch_uint(ErlNifEnv* env, ERL_NIF_TERM* items) { 258 | ERL_NIF_TERM head; 259 | // Fetch head if possible 260 | if (! enif_get_list_cell(env, *items, &head, items)) return 0; 261 | 262 | unsigned int i; 263 | double d; 264 | 265 | if(enif_get_uint(env, head, &i)) return i; 266 | if(enif_get_double(env, head, &d)) return (unsigned int) abs(d); 267 | 268 | return 0; 269 | }; 270 | 271 | long 272 | fetch_long(ErlNifEnv* env, ERL_NIF_TERM* items) { 273 | ERL_NIF_TERM head; 274 | // Fetch head if possible 275 | if (! enif_get_list_cell(env, *items, &head, items)) return 0; 276 | 277 | long i; 278 | double d; 279 | 280 | if(enif_get_long(env, head, &i)) return i; 281 | if(enif_get_double(env, head, &d)) return (long) d; 282 | 283 | return 0; 284 | }; 285 | 286 | unsigned long 287 | fetch_ulong(ErlNifEnv* env, ERL_NIF_TERM* items) { 288 | ERL_NIF_TERM head; 289 | // Fetch head if possible 290 | if (! enif_get_list_cell(env, *items, &head, items)) return 0; 291 | 292 | unsigned long i; 293 | double d; 294 | 295 | if(enif_get_ulong(env, head, &i)) return i; 296 | if(enif_get_double(env, head, &d)) return (unsigned long) abs(d); 297 | 298 | return 0; 299 | }; 300 | 301 | double 302 | fetch_double(ErlNifEnv* env, ERL_NIF_TERM* items) { 303 | ERL_NIF_TERM head; 304 | // Fetch head if possible 305 | if (! enif_get_list_cell(env, *items, &head, items)) return 0.0; 306 | 307 | long i; 308 | double d; 309 | 310 | if(enif_get_double(env, head, &d)) return d; 311 | if(enif_get_long(env, head, &i)) return (double) i; 312 | 313 | return 0.0; 314 | }; 315 | 316 | char* 317 | fetch_string(ErlNifEnv* env, ERL_NIF_TERM* items) { 318 | ERL_NIF_TERM head; 319 | char* buffer; 320 | 321 | // Fetch head if possible 322 | if (! enif_get_list_cell(env, *items, &head, items)) { 323 | buffer = (char*) malloc(1); 324 | buffer[0] = 0; 325 | return buffer; 326 | } 327 | 328 | unsigned int bodylen = 0; 329 | 330 | // Try to fetch atom 331 | if (enif_get_atom_length(env, head, &bodylen, ERL_NIF_LATIN1)) { 332 | buffer = (char*) malloc(bodylen + 1); 333 | buffer[0] = 0; 334 | enif_get_atom(env, head, buffer, bodylen + 1, ERL_NIF_LATIN1); 335 | return buffer; 336 | } 337 | 338 | // If it is not atom, try to fetch iolist. Strings, deep lists, binaries should fit well here 339 | ErlNifBinary bin; 340 | if (enif_inspect_iolist_as_binary(env, head, &bin)) { 341 | bodylen = bin.size; 342 | buffer = (char*) malloc(bodylen + 1); 343 | memcpy(buffer, bin.data, bodylen); 344 | buffer[bodylen] = 0; 345 | return buffer; 346 | } 347 | 348 | // If we get here there is no string-like term in the head, so just allocate and return "\0" 349 | buffer = (char*) malloc(1); 350 | buffer[0] = 0; 351 | return buffer; 352 | }; 353 | 354 | 355 | // dummy upgrade 356 | static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) { 357 | return 0; 358 | }; 359 | 360 | 361 | // Erlang to C function mapping 362 | static ErlNifFunc io_libc_funcs[] = { 363 | {"fwrite", 2, io_libc_fwrite}, 364 | {"utc_to_datetime", 1, utc_to_datetime} 365 | }; 366 | 367 | ERL_NIF_INIT(io_libc, io_libc_funcs, NULL, NULL, upgrade, NULL) 368 | --------------------------------------------------------------------------------