What is the best practice for this?
You can't do it in one time, I suppose you'll have to use FFI, so my idea was to port Xash3D (Half-Life engine), but its too complicated, the modules are too big and depend of globals, void functions, hash tables, structs with function pointers, etc.
So how would you do it bit by bit?
Throw it all away and start from scratch. Or join somebody else's work on a game engine in Nim. ;-)
C projects are big because C has no decent abstraction mechanisms, it usually doesn't do all that much.
You have to do some refactoring on the C side first to make the C code usable from the C side. You can't just port some big C project directly to Nim in one go. What I would recommend is make the entry point in Nim as a first step. Like take the main function in C, rename it to cMain or whatever, and call that from Nim. Make sure that works.
After that you move functionality to Nim slowly.
You should also make sure that you can call Nim from C so that when you find a relatively self-contained module you can port that to Nim and expose it to C.
The call stack while porting will look like this:
the modules are too big...
This is not really a problem, you just have to know how to navigate the code.
and depend of globals
This is not fun, but at first it's a good idea to just create a catch-all "context" struct and put all the globals in that. Then you pass that struct to each function. Depending on how you want to go about this, you can break that struct into smaller structs in C or just replicate the same thing in Nim and pass that to C functions.
void functions
What's wrong with void functions? If you're talking about functions that don't return values but take in pointer for result argument, that's not a problem, just create a specific type that's only returned from that function and used by its callers. Don't fear more types, they're your friends at this points. Do as much as you can in types and reduce the code / pointers / whatever.
hash tables, structs with function pointers
These are also not fun. What I recommend is move all hash table types, structs with function pointers etc. to their own module and make sure thay're only accessed through functions that you control. Nobody accesses them directly. Nobody.
So at first in Nim if you need to use those they're opaque types, you use them through the functions exposed from C. Same thing goes for the rest of the C codebase. You need to access a field of the struct? Create a access_struct_field(struct*, result*) function, and make sure only that is used everywhere. You need to create the struct? Make a create_struct() function that returns an opaque pointer. Nobody accesses the struct directly, only through the functions you provide.
You can even make it a seperate library with only needed functions exposed so that you can be sure.
Then when you're sure everyone accesses that stuff through functions you control, just swap those functions with Nim version! Easy peasy, lemon squeezy. I mean, not really, but you got the gist (by the way, I'm serious. NOBODY accesses those weird types directly until the whole codebase is in one language again).
Oh, and you can't use cool Nim features such as destructors while you have C code in the codebase, so you'll have to create deinit procs for you Nim types expose them. Call deinit in your destructor in Nim, and call the exposed deinit function in C.
Anyways, you'll have to be comfortable with C, I think that's pretty much clear by this point. Oh, and I'm not sure if I said this before but do not let weird types like type-specific-hash-tables and structs-with-function-pointers be accessed directly!
What I wrote above is not specific to Nim, I believe Nim has cool C interoperability features such as inlining C directly, but the points I made above goes for any porting project that will communicate over the C ABI.
Throw it all away and start from scratch.
I'm not <that> confident :P
Even if i manage to implement the level loading, it just has too many things, the platforms, console system, entity manager, renderers, networking, sound, client-server communication, prediction, etc.
You just do it? Porting code from any language to another requires you know the source language and the target language. There is no "ez" button here...
If you don't want to port, and the game engine is extensible via plugins, you could write Nim plugins and go that route. If that's not the case, your best bet is to wrap the API and interface with the engine that way.
If you really want to go about porting, well then be prepared to write the code in Nim. I wouldn't say "throw it all away" - I'd say understand C and Nim and figure out how to write idiomatic Nim to reproduce whatever the C code was doing.
Nim has it's own stdlib so you don't need to port C's obviously - and a lot of game engines written in C/C++ bring their own stdlib.
I've been experimenting with building a game engine in Nim (and lately Odin) for years now. I started with zero knowledge and have successfully implemented most subsystems of a modern game engine in Nim. It's yak shaving for sure, but it's also taught me quite a bit about game engine dev and programming. I'm currently at the point where I can write my own parallelized 3d game engine and already have made quite a bit of progress on the current iteration - https://github.com/Tail-Wag-Games/frag/
@kobi - yes you can certainly go the route of generating code via tooling like c2nim. c2nim even has a cpp mode - and you can use c2nim to port implementations too.
Still - if you're porting code to Nim, you most likely don't want to just port the code to Nim. You most likely want to write idiomatic Nim code, which means understanding Nim syntax and semantics as well as the languages features. It also requires a deep understand of C/C++ (depending on what you're porting). Since memory management in C is manual and in C++ memory management has all sorts of ins-and-outs it really is required to understand what the C or C++ code is doing if you're going to be able to successfully port some large, complex set of code.
So, to sum my perspective - automated tooling is great to get you to a certain point but you'll most likely what to re-write that generated code anyway, so you might as well just write it yourself - at least from my perspective and years of experience porting software from C and C++ and other languages to Nim.
Now this does not come from C per se but if you want that code to get ported in a way that is a very close drop-in replacement the first thing you want to do is write tests for everything that is important, including performance.
Then I would suggest to isolate some core mechanisms that are written in C and expose them as a library. Then you can program with those in Nim, which is already great. Finally, one by one, replace all those. Then refactor the resulting Nim code.
Can't test arc on play.nimlang I don't think but the principle is the same: use destructors. https://play.nim-lang.org/#ix=3YCo
{.emit:"""
typedef struct Foo{
int id;
} Foo;
void freeFoo(Foo* f){
printf("freeing %d\n",f->id);
}
""".}
type Foo{.importc.}= object
id:int
proc `=destroy`(f: var Foo){.importc:"freeFoo".}
proc main()=
var f:Foo
f.id=5
main()