Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added more c apis and capi ssl version with capi examples #1395

Merged

Conversation

cirospaciari
Copy link
Contributor

@cirospaciari cirospaciari commented Jan 5, 2022

This adds a complete useful capi with ssl suport and most examples added.

It's a step to solve this issue: #1192
Opaque C interface for Swift, Rust, etc

For now its just a wrapper and can be optimized a lot, but its a start.

make capi, and make capi_examples for build added

@cirospaciari cirospaciari changed the title added more apis and ssl version with most examples added more capis and capi ssl version with most examples Jan 5, 2022
@cirospaciari cirospaciari changed the title added more capis and capi ssl version with most examples added more c apis and capi ssl version with capi examples Jan 5, 2022
@cirospaciari
Copy link
Contributor Author

cirospaciari commented Jan 6, 2022

instead of uws_app_listen(SSL, style i wrote in uws_ssl_... style because responses and websockets has diferent types while in App and SSLApp, with a constant with -O3 optimization and a static library i think the code will be generated ok and inlined, but i want to use this capi in Ruby with FFI and i dont want to have a if to check pointer conversions at runtime>

In dynamic typed languages as Ruby, Python and Lua, UWS::App and UWS::SSLApp will be easy to implement and switch between App and SSLApp and static typed languages with generics is easy too to create variants (like in rust)

In pure C with different structs when matters, keep me/developers safe to make mistakes like passing a ssl version of response in uws_res_end instead of uws_ssl_res_end without overhead, if someone wants to use the capi in C instead of integrate with a dynamic library in other language, and don't want to write different codes to switch between App and SSLApp, a header with macros would solve this problem in a safe way.

I tested with autocannon -c 100 -d 40 -p 10 http://localhost:3000/ and the CAPIHelloWorld and HelloWorld produces the same/similar performance numbers, Ruby integrates very well currently i'm testing with FFI and i will integrate with Python and Rust with FFI too, source code available in my github (just for linux for now, will be cross platform solution later)

@ghost
Copy link

ghost commented Jan 7, 2022

I like the energy but there are a few problems -

I've fixed the existing experimental CAPI and it now works with identical performance as the C++ one so the idea is very much valid.

Problems:

  • You add copies and dynamic memory allocations everywhere via that toNullTerminated - that's a big no no
  • SSL should be an argument, we don't want the whole API duplicated for every protocol, esp. not when QUIC is yet another protocol
  • Benchmarking with autocannon is 10000000% invalid. Use uSockets/http_load_test. Performance MUST BE IDENTICAL as C++, there can be NO DIFFERENCE AT ALL, or the whole thing must be thrown in the garbage.
  • Dynamic linking with .so is a big no no, must be a static library built with LTO optimization otherwise performance goes to shit.
  • You don't need to make tons of examples, the important is getting the library itself done.
  • I like the name libuwebsockets.so, but it should be libuwebsockets.a

It is extremely important that perf. remains identical to the original (this is the case with existing experiment).

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Jan 7, 2022

Yea the toNullTerminated was a mistake for sure but is easy replaceable.
I created the examples while developing and using the C++ examples as a reference, so I include them.

About the performance testing i will look on http_load_test, i used autocannon and wrk for http because it's the tools I knew, if you can tell me the reasons why using autocannon would be invalid I would be grateful (so I don't make that mistake again).

I will take a look in the last commit you made in the current experiment, and will change the SSL version to be a parameter.

I will remove the toNulllTermined and improve to get the same performance in the C wrapper.

CAPI (my version)
./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 299293.000000
Req/sec: 285187.250000
Req/sec: 298784.750000
Req/sec: 297208.500000
Req/sec: 299072.500000
Req/sec: 291401.000000
^C
C++ original version:
./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 328924.500000
Req/sec: 330413.000000
Req/sec: 332356.750000
Req/sec: 332048.000000
Req/sec: 331578.500000

Its clear the performance hit when using http_load_test, i will stick with http_load_test for now and the future.

@ghost
Copy link

ghost commented Jan 7, 2022

I want to test with the SSL as argument in my experiment to check one extra time it remains the same perf.

Also another thing:

  • You don't need to wrap libusockets - that library is already C, so just leave it be. You see that libusockets use SSL as first argument so libuwebsockets should as well.

@ghost
Copy link

ghost commented Jan 7, 2022

I also want to make a very basic minimal test of a Rust wrapper with a nice interface much like the one in C++ to see if that is even viable - if so then it could make sense to create a Rust crate that just works out of the box. And maybe even a Swift one - then the reach of the project can expand without being limited by scripting performance loss.

@cirospaciari
Copy link
Contributor Author

I want to test with the SSL as argument in my experiment to check one extra time it remains the same perf.

Also another thing:

  • You don't need to wrap libusockets - that library is already C, so just leave it be. You see that libusockets use SSL as first argument so libuwebsockets should as well.

I tested with the SSL parameter and the performance numbers are the same, after and before.
I tested in rust too and i have some little variation but i think its margin of error because the difference its not consistent a more complex sample testing maybe highlight something, but for now seems good.

#define SSL 1

void get_handler(uws_res_t *res, uws_req_t *req) {
    uws_res_end(SSL, res, "Hello CAPI!", 11);
}

void listen_handler(void *listen_socket) {
    if (listen_socket) {
        printf("Listening on port now\n");
    }
}

int main() {
    uws_app_t *app = uws_create_app(SSL, (struct us_socket_context_options_t){
        /* There are example certificates in uWebSockets.js repo */
	    .key_file_name = "../misc/key.pem",
	    .cert_file_name = "../misc/cert.pem",
	    .passphrase = "1234"
    });
    uws_app_get(SSL, app, "/*", get_handler);
    uws_app_listen(SSL, app, 3000, listen_handler);
    uws_app_run(SSL, app);
}

@ghost
Copy link

ghost commented Jan 7, 2022

Yeah as long as it is a static library compiled with LTO it should constant fold that if statement

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Jan 7, 2022

I added length in all points that i needed pass a string_view and it bring the performance up tothe same as the C++ version.

I also add some changes in example.rs now examples/RustHelloWorld.rs, now i able to compile on my machine without warnings and errors.

I removed all copies and dynamic memory allocations, but i'm trying to get the reader for do a upgrade (i want to make it work in C and add to Rust for testing) in my last commit UpgradeSync/UpgradeAsync

The header will never returns because its a std::string_view, i'm trying something like this:

const char *uws_req_get_header(uws_req_t *res, const char *lower_case_header, size_t lower_case_header_length)
{
      uWS::HttpRequest *uwsReq = (uWS::HttpRequest *)res;
      std::string_view header = uwsReq->getHeader(std::string_view(lower_case_header, lower_case_header_length));
      return std::string(header).c_str();
}

and the call in UpgradeSync.c:

const char* header= uws_req_get_header(request, "sec-websocket-extensions", 24);

The string_view will get out of scope and free, how can i accomplish this or with strategy i should use to get the headers without dynamic alloc or copy? i could pass a buffer and a buffer size and copy the info back, but i want help to know if exists a better alternative.

After this i will add the SSL params in all points.

@ghost
Copy link

ghost commented Jan 8, 2022

I'm playing with something like this:

fn main() {
    App::new().get(|res, req| {
        //res.end("Hello world!");
    }).listen(|listenSocket| {
        if listenSocket != 0.0 {
            println!("Listening now!");
        }
    }).run();
}

@cirospaciari
Copy link
Contributor Author

I'm playing with something like this:

fn main() {
    App::new().get(|res, req| {
        //res.end("Hello world!");
    }).listen(|listenSocket| {
        if listenSocket != 0.0 {
            println!("Listening now!");
        }
    }).run();
}

Cool very close to my ruby version

UWS::App.new()
.get("/", lambda {|response, request| response.end("Hello World uWS from Ruby!")})
.listen(8082, lambda {|socket, config| puts "Listening on port #{config.port}" })
.run()

@cirospaciari
Copy link
Contributor Author

Performance impacts before are from char* to string_view convertions without length and the use of dynamic library it self (about of 13% of performance impact), now perfoms the same

CAPI

cirospaciari@gl702vsk:~/Desktop/teste/uWebSockets/uSockets$  ./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 318886.250000
Req/sec: 321785.750000
Req/sec: 320193.250000
Req/sec: 313785.750000
^C

C++

cirospaciari@gl702vsk:~/Desktop/teste/uWebSockets/uSockets$ ./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 313398.750000
Req/sec: 312036.750000
Req/sec: 317940.750000
Req/sec: 320571.250000
^C

I will add the SSL parameters now and add a more complex sample using Uprade and Broadcast with Rust and i will collect some performance data and compare Rust, CAPI and C++. I think if we static link we should have zero or almost zero performance impact with a more OOP like Rust wrapper, i will do a stress test and measuring if in some place the compiler will not fold the conditional (i belive if we create a crate with the wrapper maybe the compiler will not optimize but lets do some testing)

@cirospaciari
Copy link
Contributor Author

Without heavy modifications i could archive the code bellow without performance impact:

extern "C" fn listen_handler(
    _listen_socket: *mut ::std::os::raw::c_void,
    config: UwsAppListenConfigT,
) {
    println!("Listening on port {}", config.port);
}

extern "C" fn get_handler(res: *mut UwsResT, _: *mut UwsReqT) {
    unsafe {
        let message = ::std::ffi::CString::new("Hello Rust!").expect("");
        uws_res_end(res, message.as_ptr(), 11, false);
    }
}


fn main() {
    App::new()
        .get("/", get_handler)
        .listen(3000, listen_handler)
        .run();
}

For archive what you expected in rust passing a user_data: void* is needed:

fn main() {
    App::new()
        .get("/", |res, _req| {
            res.end("Hello Rust!");
        })
        .listen(3000, |_listen_socket, config| {
            println!("Listening on port http://127.0.0.1:{}", config.port);
        })
        .run();
}

No visible performance loss:

CAPI

cirospaciari@gl702vsk:~/Desktop/teste/uWebSockets/uSockets$ ./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 311942.000000
Req/sec: 319498.250000
Req/sec: 323747.000000
^C

Rust

cirospaciari@gl702vsk:~/Desktop/teste/uWebSockets/uSockets$ ./http_load_test 40 localhost 3000
Running benchmark now...
Req/sec: 319163.000000
Req/sec: 323562.500000
Req/sec: 323056.250000
^C

@ghost
Copy link

ghost commented Jan 8, 2022

One thing missing is that all functions that take a function must also take a void pointer so closures can be set. That's a little tricky, I need to look at Rust pointers. If that works without perf loss then everything should be set

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Jan 8, 2022

One thing missing is that all functions that take a function must also take a void pointer so closures can be set. That's a little tricky, I need to look at Rust pointers. If that works without perf loss then everything should be set

I was typing just that haha, yeah is tricky but is useful in other languages too, i used this in "uws_create_timer" as a helper and in uws_res_on_aborted, uws_res_on_data and uws_res_on_writable

I will keep my branch updated

Edit: Added the void pointers, was i said before i cannot measure any performance loss with http_load_test, after a little break i will start working in the SSL parameter

the following is working in capi/examples/RustHelloWorld.rs
make default will create HelloWorld (capi) and RustHelloWorld
make rust will only create RustHelloWorld

fn main() {
    App::new()
        .get("/", |res, _req| {
            res.end("Hello Rust!");
        })
        .listen(3000, |_listen_socket, config| {
            println!("Listening on port http://127.0.0.1:{}", config.port);
        })
        .run();
}

@uNetworking uNetworking deleted a comment from Prozi Oct 11, 2022
@mohan-win
Copy link

@cirospaciari , Thanks for the awesome effort, wondering if you have any idea when this PR would get merged ?

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Oct 28, 2022

@cirospaciari , Thanks for the awesome effort, wondering if you have any idea when this PR would get merged ?

No idea I still working on this, today I will add more fixes, integrations, updates and push again.

You can follow my progress on my project cirospaciari/socketify.py#10

I hope until end of this year I get this fully working with HTTP3 too, I can just work few hours in a couple of months but I will get more effort until December

@mohan-win
Copy link

@cirospaciari , Thanks for the awesome effort, wondering if you have any idea when this PR would get merged ?

No idea I still working on this, today I will add more fixes, integrations, updates and push again.

You can follow my progress on my project cirospaciari/socketify.py#10

I hope until end of this year I get this fully working with HTTP3 too, I can just work few hours in a couple of months but I will get more effort until December

Got it, thanks...

@uNetworkingAB
Copy link
Contributor

I need to figure out how this plays along with the c wrapper used by bun. Maybe the bun wrapper was built from this PR? Anyways one of the two are great additions

@uNetworkingAB
Copy link
Contributor

https://github.com/oven-sh/bun/blob/0bfd00e734ebe6093fd69badbc1d733d039f3d6a/src/deps/libuwsockets.cpp

If you look there, does that look like a copy of this PR, or a completely separate wrapper?

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Oct 28, 2022

https://github.com/oven-sh/bun/blob/0bfd00e734ebe6093fd69badbc1d733d039f3d6a/src/deps/libuwsockets.cpp

If you look there, does that look like a copy of this PR, or a completely separate wrapper?

Looks a lot with my solution but have some differences, like using memcpy, my wrapper seems to be more feature complete, I will update with the last version and add more things today after my lunch break, I think maybe is a copy of my PR at some point but if is the case I will only be glad for helping Bun

@uNetworkingAB
Copy link
Contributor

I also thought the bun one looked like your PR

@cirospaciari
Copy link
Contributor Author

cirospaciari commented Oct 28, 2022

I need to figure out how this plays along with the c wrapper used by bun. Maybe the bun wrapper was built from this PR? Anyways one of the two are great additions

I opened an discussion on Bun offering help to add more features, i think we can use this PR as an official C wrapper in future and converge Bun.sh to use the same version. This weekend i will add Http3 Support.
I was already using in some projects this wrapper and is doing great.

EDIT:
@uNetworkingAB
Jarred responded and it's a copy of this PR but with some additions like i thought (oven-sh/bun#1414) I will contribute to Bun and keeping contributing with this wrapper, so we can converge in one solution.

@uNetworkingAB
Copy link
Contributor

Great, could you remove all changes that aren't related to capi folder. This PR should really only (initially at least) add a new folder.

@cirospaciari
Copy link
Contributor Author

Great, could you remove all changes that aren't related to capi folder. This PR should really only (initially at least) add a new folder.

Sure i will remove all changes that are not in capi folder, but i thinking in expose cork and uncork but i could do it in another fork.

@cirospaciari
Copy link
Contributor Author

@uNetworkingAB reverted any changes outside capi folder, sorry for the mess.

@uNetworkingAB uNetworkingAB merged commit 3f72aea into uNetworking:master Oct 29, 2022
@uNetworkingAB
Copy link
Contributor

Very kickass. Feel free to make a new PR from master with further changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants